중요한 배경 읽기 : Agner Fog의 microarch pdf 및 아마도 모든 프로그래머가 메모리에 대해 알아야 할 Ulrich Drepper도 있습니다 . 의 다른 링크도 참조하십시오x86태그 위키, 특히 Intel의 최적화 매뉴얼 및 David Kanter의 Haswell 마이크로 아키텍처 분석 (다이어그램 포함) .
매우 멋진 과제; 실제 코드에서 중요하지 않은 많은 트릭을 배우면서 학생들이 코드를 최적화하도록 요청받은gcc -O0
곳보다 훨씬 낫습니다 . 이 경우 CPU 파이프 라인에 대해 배우고이를 맹목적인 추측뿐만 아니라 최적화 해제 노력을 안내하는 데 사용하라는 요청을받습니다. 이 중 가장 재미있는 부분은 각 비관을 의도적 인 악의가 아니라 "비공식적 무능력"으로 정당화하는 것입니다.
과제 문구 및 코드 문제 :
이 코드에 대한 uarch 특정 옵션이 제한됩니다. 배열을 사용하지 않으며 많은 비용이 exp
/ log
라이브러리 함수를 호출 합니다. 어느 정도의 명령어 레벨 병렬 처리를 갖는 확실한 방법은 없으며 루프로 수행되는 종속성 체인은 매우 짧습니다.
종속성을 변경하기 위해 식을 다시 정렬하여 속도를 낮추고 종속성 (위험)에서 ILP 를 줄이기 위해 시도한 답변을보고 싶습니다 . 나는 그것을 시도하지 않았습니다.
인텔 샌디 브릿지 제품군 CPU는 병렬 처리를 위해 많은 트랜지스터와 전력을 소비 하고 고전적인 RISC 주문 파이프 라인에 문제가 되는 위험 (종속성)을 피하는 공격적인 비 순차적 설계입니다 . 일반적으로 속도를 늦추는 기존의 유일한 위험 요소는 처리 시간이 대기 시간에 의해 제한되는 RAW "진정한"종속성입니다.
레지스터 이름 변경으로 인해 레지스터에 대한 WAR 및 WAW 위험 은 거의 문제가되지 않습니다 . (popcnt
/lzcnt
/제외tzcnt
,쓰기 전용 인 경우에도 인텔 CPU에 대한 대상 이 잘못된 종속성이 있음 ( 예 : WAW가 RAW 위험 + 쓰기로 처리됨)) 메모리 주문의 경우 최신 CPU는 저장소 큐를 사용 하여 폐기 될 때까지 캐시에 커밋을 지연시키고 WAR 및 WAW 위험을 피 합니다.
왜 Agner의 지시 표와 다른 mulss가 Haswell에서 3 주기만 소요됩니까? FP 도트 제품 루프에서 레지스터 이름 변경 및 FMA 대기 시간 숨기기에 대해 자세히 설명합니다.
"i7"브랜드 이름은 Nehalem (Core2의 후속)과 함께 소개 되었으며 일부 Intel 매뉴얼은 Nehalem을 의미하는 것처럼 보일 때 "Core i7"이라고 말하지만 Sandybridge 및 이후의 마이크로 아키텍처에 대해서는 "i7"브랜딩 을 유지했습니다 . SnB는 P6- 패밀리가 새로운 종인 SnB- 패밀리로 진화했을 때입니다 . 여러 가지면에서 Nehalem은 Sandybridge보다 Pentium III과 더 공통적입니다 (예 : 레지스터 읽기 스톨 및 ROB 읽기 스톨은 실제 레지스터 파일을 사용하도록 변경 되었기 때문에 SnB에서 발생하지 않습니다. 또한 uop 캐시 및 다른 내부 UOP 형식). "i7 아키텍처"라는 용어는 유용하지 않습니다SnB 계열을 Nehalem과 그룹화하는 것은 의미가 없지만 Core2는 그룹화하지 않습니다. (Nehalem은 여러 코어를 함께 연결하기위한 공유 포괄 L3 캐시 아키텍처를 도입했습니다. 또한 통합 GPU도 지원합니다. 따라서 칩 수준에서는 더 의미가 있습니다.)
악마의 무능력이 정당화 할 수있는 좋은 아이디어의 요약
비현실적으로 능력이 부족한 사람들조차도 분명히 쓸모없는 일이나 무한 루프를 추가 할 것 같지 않으며 C ++ / 부스트 클래스로 혼란을 일으키는 것은 과제의 범위를 벗어납니다.
- 단일 공유
std::atomic<uint64_t>
루프 카운터 가있는 다중 스레드 이므로 올바른 총 반복 횟수가 발생합니다. 원자 uint64_t는 특히 나쁘다 -m32 -march=i586
. 보너스 포인트의 경우, 정렬이 잘못되고 페이지 경계가 고르지 않은 분할 (4 : 4 아님)로 교차하도록 정렬하십시오.
- 다른 비 원자 변수-> 메모리 차수 오 추측 파이프 라인에 대한 허위 공유 및 추가 캐시 누락.
-
FP 변수 를 사용 하는 대신 부호 비트를 뒤집기 위해 0x80으로 상위 바이트를 XOR하여 상점 전달 스톨을 발생시킵니다 .
- 보다 무거운 것을 사용하여 각 반복의 시간을 독립적으로 지정하십시오
RDTSC
. 예를 들어, CPUID
/ RDTSC
또는 시스템 호출을 만드는 시간 함수. 직렬화 명령어는 본질적으로 파이프 라인에 적합하지 않습니다.
- 상수를 곱하여 역수로 나눕니다 ( "읽기 쉬움"). div가 느리고 완전히 파이프 라인되지 않습니다.
- AVX (SIMD)를 사용하여 곱하기 / sqrt를 벡터화하지만
vzeroupper
스칼라 수학 라이브러리 exp()
및 log()
함수 를 호출하기 전에 사용하지 않으면 AVX <-> SSE 전환이 중단됩니다 .
- RNG 출력을 링크 된 목록 또는 순서가 잘못된 순회 배열로 저장하십시오. 각 반복의 결과와 동일하며 끝에서 합계입니다.
또한이 답변에서 다루었지만 요약에서 제외되었습니다. 파이프 라인되지 않은 CPU에서는 느리거나, 비신사적인 무능력으로도 타당하지 않은 제안. 예를 들어 분명히 다른 / 더 나쁜 asm을 생성하는 많은 gimp-the-compiler 아이디어.
멀티 스레드 심하게
OpenMP를 사용하면 반복 횟수가 적고 속도 게인보다 오버 헤드가 많은 다중 스레드 루프에 사용할 수 있습니다. 그러나 몬테카를로 코드에는 실제로 병렬 처리를하기에 충분한 병렬 처리가 있습니다. 각 반복을 느리게 만드는 데 성공하면 (각 스레드 payoff_sum
는 끝에 추가 된 partial을 계산합니다 ). #omp parallel
그 루프에서 비관 화가 아닌 최적화 일 것입니다.
다중 스레드이지만 두 스레드가 동일한 루프 카운터를 공유 atomic
하도록합니다 ( 증가 와 함께 총 반복 횟수가 정확함). 이것은 논리적으로 논리적으로 보입니다. 이는 static
변수를 루프 카운터로 사용하는 것을 의미 합니다. 이것은 atomic
for 루프 카운터의 사용을 정당화 하고 실제 캐시 라인 핑퐁을 생성합니다 (스레드가 하이퍼 스레딩을 사용하여 동일한 물리적 코어에서 실행되지 않는 한 느리지 는 않습니다 ). 어쨌든 이것은에 대한 비경쟁 사례보다 훨씬 느립니다 lock inc
. 그리고 32 비트 시스템에 대한 문제 lock cmpxchg8b
를 원자 적으로 증가 uint64_t
시키기 위해서는 하드웨어가 원자를 중재하는 대신 루프에서 다시 시도해야합니다 inc
.
또한 여러 스레드가 동일한 캐시 라인의 다른 바이트에 개인 데이터 (예 : RNG 상태)를 유지하는 허위 공유를 만듭니다 . (perf 카운터를 포함하여 그것에 관한 인텔 튜토리얼) . 여기에는 마이크로 아키텍처 관련 측면이 있습니다 . 인텔 CPU 는 메모리 오더 가 발생 하지 않는 것으로 추측하고 , 적어도 P4에서이를 감지하기 위한 메모리 순서 시스템 클리어 퍼프 이벤트가 있습니다. 벌금은 Haswell에서 크지 않을 수 있습니다. 그 링크가 지적한 바와 같이, lock
ed 명령어는 잘못 추측하지 않고, 이것이 일어날 것이라고 가정합니다. 일반로드는 다른 코어가로드가 실행될 때와 프로그램 순서에서 종료 될 때 사이에 캐시 라인을 무효화하지 않을 것이라고 추측합니다 (를 사용하지 않는 한pause
). lock
ed 지침이 없는 진정한 공유 는 일반적으로 버그입니다. 원자가 아닌 공유 루프 카운터를 원자 사례와 비교하는 것이 흥미로울 것입니다. 실제로 비관하려면 공유 원자 루프 카운터를 유지하고 다른 변수에 대해 동일하거나 다른 캐시 라인에서 잘못된 공유를 유발하십시오.
임의의 uarch 특정 아이디어 :
예측할 수없는 분기를 도입 할 수 있으면 코드가 크게 비관 화됩니다. 최신 x86 CPU는 파이프 라인이 매우 길기 때문에 잘못된 예측 비용은 ~ 15주기 (uop 캐시에서 실행할 때)입니다.
의존성 체인 :
이것이 과제의 의도 된 부분 중 하나라고 생각합니다.
여러 개의 짧은 종속성 체인 대신 하나의 긴 종속성 체인이있는 작업 순서를 선택하여 명령 수준 병렬 처리를 활용하는 CPU의 기능을 제거하십시오. 컴파일러는을 사용하지 않는 한 FP 계산의 연산 순서를 변경할 수 없습니다 -ffast-math
. 결과는 다음과 같습니다.
이를 효과적으로 적용하려면 루프 전달 종속성 체인의 길이를 늘리십시오. 그럼에도 불구하고 아무것도 눈에 띄지 않습니다. 작성된 루프에는 루프 전달 종속성 체인이 매우 짧으며 FP 추가 만 있습니다. (3주기). 다중 반복은 payoff_sum +=
이전 반복이 끝나기 전에 시작될 수 있기 때문에 한 번에 계산을 수행 할 수 있습니다 . ( log()
및 exp
많은 지시를하지만, 더 이상 많이 하 스웰은 순차적 (out-of-order)의 창의 병렬 찾는 : ROB의 크기 = 192 융합 영역의 마이크로 연산 및 스케줄러 크기 = 60 융합 영역의 마이크로 연산을. 현재 반복의 실행이 다음 반복에서 명령을 실행할 공간을 충분히 확보 할 수있을 정도로 빨리 진행되는 즉시, 입력이 준비된 (예 : 독립적 / 별도의 뎁 체인) 부분은 이전 명령이 실행 단위를 떠날 때 실행을 시작할 수 있습니다. (예 : 처리량이 아닌 지연 시간에 병목 현상이 발생하여)
RNG 상태는 거의 확실히 루프 캐리어 의존성 체인보다 길다 addps
.
더 느리거나 더 많은 FP 작업 사용 (특히 더 많은 나누기) :
0.5를 곱하는 대신 2.0으로 나눕니다. FP 곱셈은 인텔 디자인에서 파이프 라인이 많으며 Haswell 이상에서 0.5c 당 처리량이 1 개입니다. FP divsd
/ divpd
는 부분적으로 만 파이프 라인 됩니다. (Skylake는 divpd xmm
Nehalem (7-22c)에서 파이프 라인되지 않고 13-14c 대기 시간으로 4c 당 처리량이 인상적이지만 ).
는 do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
분명히 그렇게 명확하게에 적합한 것, 거리를 테스트한다 sqrt()
그것. : P ( sqrt
보다 느립니다 div
).
@Paul Clayton이 제안한 것처럼, 연관 / 분산 등가물로 표현식을 다시 작성 -ffast-math
하면 컴파일러가 다시 최적화 하지 않는 한 더 많은 작업을 수행 할 수 있습니다. (exp(T*(r-0.5*v*v))
될 수 exp(T*r - T*v*v/2.0)
있습니다. 실수에 대한 수학은 연관성 이 있지만 , overflow / NaN ( -ffast-math
기본적으로 켜져 있지 않은)을 고려하지 않아도 부동 소수점 수학은 그렇지 않습니다 . 매우 털이 중첩 된 제안 은 Paul의 의견 을 참조하십시오 pow()
.
계산을 아주 작은 수로 축소 할 수있는 경우 FP 수학 연산은 두 개의 정규 숫자에 대한 연산이 비정상을 생성 할 때 마이크로 코드에 트랩하기 위해 ~ 120 추가주기를 수행합니다 . 정확한 숫자와 세부 사항은 Agner Fog의 microarch pdf를 참조하십시오. 곱하기가 많으므로 배율이 제곱되어 0.0으로 언더 플로됩니다. 나는 무능한 (심지어 악마적인), 의도적 인 악의만으로 필요한 스케일링을 정당화 할 수있는 방법을 보지 못합니다.
내장 함수를 사용할 수있는 경우 ( <immintrin.h>
)
movnti
캐시에서 데이터를 제거하는 데 사용 합니다 . 악마 : 그것은 새롭고 약한 순서이므로 CPU가 더 빨리 실행되도록해야합니까? 또는 누군가가 정확히이 작업을 수행 할 위험이있는 경우 관련 질문을 참조하십시오 (일부 위치 만 핫한 흩어진 쓰기의 경우). clflush
악의 없이는 불가능할 것입니다.
FP 수학 연산간에 정수 셔플을 사용하여 바이 패스 지연을 발생시킵니다.
적절하게 사용하지 않고 SSE와 AVX 명령어 혼합 vzeroupper
전 스카이 레이크의 많은 원인 노점 (및 다른 페널티 스카이 레이크에서 ). 그럼에도 불구하고 벡터화는 스칼라보다 심하게 나빠질 수 있습니다 (256b 벡터를 사용하여 한 번에 4 개의 Monte-Carlo 반복에 대해 add / sub / mul / div / sqrt 연산을 수행하여 저장 한 것보다 벡터에 데이터를 셔플하는 데 더 많은주기가 스칼라보다 나빠질 수 있음) . 추가 / 서브 / mul 실행 단위는 완전히 파이프 라인되고 전체 너비이지만 256b 벡터의 div 및 sqrt는 128b 벡터 (또는 스칼라)만큼 빠르지 않으므로 속도가 크게 향상되지 않습니다double
.
exp()
그리고 log()
그 부분은 다음 결과는 벡터에 다시 셔플, 벡터 요소는 스칼라로 다시 추출하여 별도로 라이브러리 함수를 호출 할 필요하므로, 하드웨어를 지원하지 않습니다. libm은 일반적으로 SSE2 만 사용하도록 컴파일되므로 스칼라 수학 명령어의 레거시 -SSE 인코딩을 사용합니다. 코드에서 256b 벡터와 호출 exp
을 vzeroupper
먼저 사용 하지 않으면 중단 됩니다. 돌아온 후, vmovsd
다음 벡터 요소를 인수로 설정하는 것과 같은 AVX-128 명령어 exp
도 정지됩니다. 그런 다음 exp()
SSE 명령을 실행할 때 다시 정지합니다. 이것이 바로이 질문에서 일어난 일로 10 배의 속도 저하를 일으 킵니다. (@ZBoson 감사합니다).
이 코드에 대해서는 Nathan Kurz의 Intel의 math lib vs. glibc 실험을 참조하십시오 . 향후 glibc는 벡터화 된 구현 등을 제공 할 것입니다 .exp()
IvB 이전 또는 esp. Nehalem, gcc가 16 비트 또는 8 비트 연산과 32 비트 또는 64 비트 연산으로 부분 레지스터 스톨을 발생 시키도록하십시오. 대부분의 경우 gcc는 movzx
8 또는 16 비트 작업 후에 사용 하지만 다음은 gcc가 수정 ah
한 다음 읽는 경우입니다.ax
(인라인) asm으로 :
(인라인) asm을 사용하면 uop 캐시가 손상 될 수 있습니다. 3 개의 6uop 캐시 라인에 맞지 않는 32B 코드 덩어리는 uop 캐시에서 디코더로 전환합니다. 내부 루프 내부의 분기 대상에서 몇 개의 긴 ALIGN
바이트 nop
대신 많은 단일 바이트를 사용 하는 무능한 사람 nop
이 트릭을 수행 할 수 있습니다. 또는 정렬 패딩을 레이블 대신 레이블 앞에 넣습니다. : P 이것은 프론트 엔드가 병목 현상 인 경우에만 중요하며, 나머지 코드를 비관적으로 처리 한 경우에는 문제가되지 않습니다.
자체 수정 코드를 사용하여 파이프 라인 지우기 (일명 머신 핵)를 트리거하십시오.
8 비트에 맞추기에는 너무 큰 즉시 16 비트 명령어에서 LCP 스톨 이 유용하지 않을 수 있습니다. SnB 이상의 uop 캐시는 디코딩 페널티를 한 번만 지불 함을 의미합니다. Nehalem (첫 i7)에서는 28 uop 루프 버퍼에 맞지 않는 루프에서 작동 할 수 있습니다. gcc는 때때로 -mtune=intel
32 비트 명령어를 사용했을 때나 사용할 수있는 경우 에도 그러한 명령어를 생성 합니다.
타이밍에 대한 일반적인 관용구는 CPUID
(직렬화)RDTSC
입니다. 시간은과 별도로 모든 반복 CPUID
/는 RDTSC
반드시 이끌어 RDTSC
다운 일을 늦출 이전 명령으로 다시 정렬되지 않은 많은 . (실제로 현명한 시간 관리 방법은 각 반복을 개별적으로 타이밍을 추가하고 합산하는 대신 모든 반복을 함께 시간을 맞추는 것입니다).
많은 캐시 누락 및 기타 메모리 속도 저하
union { double d; char a[8]; }
일부 변수에 a 를 사용하십시오 . 바이트 중 하나에 좁은 저장소 (또는 Read-Modify-Write)를 수행 하여 저장소 전달이 중단 됩니다. (위 위키 기사는로드 / 스토어 큐를위한 다른 많은 마이크로 아키텍처 관련 내용도 다룬다). 예를 들어 연산자 대신 높은 바이트에서만 XOR 0x80을 사용하여 부호를 뒤집습니다double
-
. 비현실적으로 유능한 개발자는 FP가 정수보다 느리다는 것을 듣고 정수 연산을 사용하여 가능한 한 많이 시도합니다. (SSE 레지스터에서 FP 수학을 대상으로하는 매우 우수한 컴파일러는 이것을xorps
다른 xmm 레지스터에 상수가 있지만 x87에서 이것이 끔찍한 유일한 방법은 컴파일러가 값을 부정한다는 것을 깨닫고 다음 추가를 빼기로 대체하는 것입니다.)
을 사용 하여 volatile
컴파일 -O3
하고 사용하지 않는 경우 사용 std::atomic
하여 컴파일러가 실제로 모든 곳에서 저장 / 다시로드하도록합니다. 전역 변수 (로컬 대신)는 일부 저장소 / 다시로드를 강제하지만 C ++ 메모리 모델의 약한 순서 는 컴파일러가 항상 메모리에 엎 지르거나 다시로드 할 필요가 없습니다.
로컬 vars를 큰 구조체의 멤버로 바꾸면 메모리 레이아웃을 제어 할 수 있습니다.
패딩에 구조체의 배열을 사용하십시오 (그리고 임의의 숫자를 저장하여 존재를 정당화하십시오).
메모리 레이아웃을 선택하여 모든 것이 L1 캐시의 동일한 "세트"에서 다른 라인으로 들어가도록합니다 . 8-way 연관성입니다. 즉, 각 세트에는 8 개의 "ways"가 있습니다. 캐시 라인은 64B입니다.
로드가 다른 페이지에 대한 상점에 대해 잘못된 종속성을 가지지 만 페이지 내에서 동일한 오프셋을 갖기 때문에 정확하게 4096B를 분리하는 것이 더 좋습니다 . 공격적인 비 순차적 CPU는 메모리 명확성을 사용 하여 결과를 변경하지 않고로드 및 저장소를 다시 정렬 할 수있는시기를 파악합니다. 인텔의 구현에는로드가 일찍 시작되지 못하게하는 잘못된 양성이 있습니다. 아마도 페이지 오프셋 아래의 비트 만 검사하므로 TLB가 높은 비트를 가상 페이지에서 실제 페이지로 변환하기 전에 검사를 시작할 수 있습니다. Agner의 가이드뿐만 아니라 Stephen Canon 의 답변과 동일한 질문에 대한 @Krazy Glew의 답변 끝 부분을 참조하십시오. (앤디 글로우는 인텔 최초의 P6 마이크로 아키텍처의 설계자 중 한 명이었습니다.)
__attribute__((packed))
변수를 캐시 라인 또는 페이지 경계에 걸쳐 정렬 할 수 있도록 잘못 정렬하는 데 사용하십시오 . (따라서 하나의로드에는 double
두 개의 캐시 라인의 데이터가 필요합니다). 정렬되지 않은로드는 캐시 라인과 페이지 라인을 교차하는 경우를 제외하고 Intel i7 uarch에서 페널티가 없습니다. 캐시 라인 분할은 여전히 추가주기가 필요 합니다. Skylake는 페이지 분할로드에 대한 패널티를 100 ~ 5 사이클로 대폭 줄 입니다. (2.1.3 장) . 두 페이지를 동시에 병렬로 처리하는 것과 관련이있을 수 있습니다.
의 페이지 분할 atomic<uint64_t>
은 최악의 경우 일 것입니다. 한 페이지에서 5 바이트, 다른 페이지에서 3 바이트 또는 4 : 4 이외의 다른 경우 가운데 부분 분할조차 일부 IIRC (IIRC)에 16B 벡터가있는 캐시 라인 분할에 더 효율적입니다. alignas(4096) struct __attribute((packed))
RNG 결과를 저장하기위한 어레이를 포함하여 모든 공간을 절약 하십시오 (물론 공간 절약). 카운터 앞에 uint8_t
또는 uint16_t
을 사용하여 오정렬을 달성하십시오 .
컴파일러가 인덱스 된 어드레싱 모드를 사용할 수있게하면 uop micro-fusion을 물리치게 됩니다 . #define
s를 사용하여 간단한 스칼라 변수를로 바꿉니다 my_data[constant]
.
추가 수준의 간접 참조를 도입 할 수 있으면로드 / 저장 주소를 일찍 알 수 없으므로 더 비관적 일 수 있습니다.
연속되지 않은 순서로 배열 탐색
배열을 처음 도입하는 것에 대한 무능한 정당화를 생각해 낼 수 있다고 생각합니다. 난수 생성과 난수 사용을 분리 할 수 있습니다. 각 반복의 결과는 배열에 저장하여 나중에 요약 할 수 있습니다 (더 많은 비 구체적 무능력).
"최대 난수"의 경우, 새로운 난수를 쓰는 난수 배열에 스레드가 반복 될 수 있습니다. 난수를 소비하는 스레드는 난수를로드 할 난수 인덱스를 생성 할 수 있습니다. (여기에는 약간의 작성 작업이 있지만 마이크로 아키텍처에서는로드 주소를 조기에 알 수 있으므로로드 된 데이터가 필요하기 전에 가능한로드 대기 시간을 해결할 수 있습니다.) 다른 코어에 리더와 라이터가 있으면 메모리 순서가 잘못 될 수 있습니다. -스프레이 파이프 라인이 지워집니다 (위의 공유 사례에 대해 앞에서 설명한대로).
최대 비관 화를 위해 4096 바이트 (즉, 512 배)의 보폭으로 배열을 반복합니다. 예 :
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
따라서 액세스 패턴은 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 8208, ...입니다.
이것은 double rng_array[MAX_ROWS][512]
잘못된 순서 로 2D 배열에 액세스하기 위해 얻는 것입니다 (@JesperJuhl에서 제안한대로 내부 루프의 행 내 열 대신 행을 반복합니다). 악마의 무능력이 2D 배열을 그와 같은 치수로 정당화 할 수 있다면, 정원의 실제 무능은 잘못된 액세스 패턴으로 루핑을 쉽게 정당화 할 수 있습니다. 이것은 실제 코드에서 실제로 발생합니다.
배열이 크지 않은 경우 동일한 몇 페이지를 재사용하는 대신 다른 페이지를 많이 사용해야하는 경우 루프 범위를 조정하십시오. 페이지 전체에서 하드웨어 프리 페칭이 작동하지 않습니다. 프리 페처는 각 페이지 내에서 하나의 정방향 및 하나의 역방향 스트림을 추적 할 수 있지만 (여기서 발생하는 것임) 메모리 대역폭이 프리 페치가 아닌 상태로 포화되지 않은 경우에만 동작합니다.
페이지가 hugepage로 병합되지 않는 한 TLB 누락이 많이 발생합니다 ( Linux는 malloc
/ 와 같은 익명의 (파일 백업이 아닌) 할당에 대해 기회를 제공 new
합니다mmap(MAP_ANONYMOUS)
).
결과 목록을 저장하는 배열 대신 링크 된 목록을 사용할 수 있습니다 . 그런 다음 모든 반복에는 포인터 추적로드 (다음로드의로드 주소에 대한 RAW 실제 종속성 위험)가 필요합니다. 할당자가 잘못되면 목록 노드를 메모리에 분산시켜 캐시를 물리 칠 수 있습니다. 비현실적으로 할당 할 수없는 할당자를 사용하면 모든 노드를 자체 페이지의 시작 부분에 배치 할 수 있습니다. (예 : mmap(MAP_ANONYMOUS)
페이지를 나누거나 객체 크기를 추적하지 않고 직접 지원하여 올바르게 할당 free
)
이들은 실제로 마이크로 아키텍처가 아니며 파이프 라인과 거의 관련이 없습니다 (이들 중 대부분은 파이프 라인이 아닌 CPU의 속도 저하입니다).
다소 벗어난 주제 : 컴파일러가 더 나쁜 코드를 생성하도록 / 더 많은 작업을 수행하십시오.
사용 C ++ (11) std::atomic<int>
과 std::atomic<double>
가장 pessimal 코드. MFENCE 및 lock
ed 명령어는 다른 스레드의 경합이 없어도 상당히 느립니다.
-m32
x87 코드가 SSE2 코드보다 나쁘기 때문에 코드 속도가 느려집니다. 스택 기반 32 비트 호출 규칙은 더 많은 명령어를 사용하며 스택의 FP 인수도 같은 함수에 전달 exp()
합니다. atomic<uint64_t>::operator++
on -m32
에는 lock cmpxchg8B
루프 (i586) 가 필요합니다 . (따라서 루프 카운터에 사용하십시오! [악한 웃음]).
-march=i386
또한 비관적입니다 (@Jesper 덕분에). FP는 fcom
686보다 느립니다 fcomi
. Pre-586은 원자 64 비트 저장소 (cmpxchg 제외)를 제공하지 않으므로 모든 64 비트 atomic
op는 libgcc 함수 호출 (실제로 잠금을 사용하지 않고 i686 용으로 컴파일 됨)으로 컴파일됩니다. 마지막 단락의 Godbolt Compiler Explorer 링크에서 사용해보십시오.
sizeof ( )가 10 또는 16 인 ABI에서 정밀도를 높이고 속도를 느리게 하려면 long double
/ sqrtl
/ expl
를 사용하십시오 long double
(정렬 용 패딩 사용). (IIRC, 64 비트 윈도우 용도 8byte long double
등가 double
. (어쨌든,로드 / 10byte의 점포 (80bit) FP 오퍼랜드 7분의 4 마이크로 연산, 비교이다 float
또는 double
단지 각 UOP 1 촬영 fld m64/m32
/를 fst
).로 x87 강제 long double
처치 자동 벡터화에도 마찬가지로 gcc -m64 -march=haswell -O3
.
atomic<uint64_t>
루프 카운터를 사용 하지 않는 경우 루프 카운터를 long double
포함한 모든 것에 사용하십시오 .
atomic<double>
컴파일하지만 읽기 수정 쓰기 작업 +=
은 64 비트에서도 지원되지 않습니다. atomic<long double>
원자 적로드 / 스토어에 대해서만 라이브러리 함수를 호출해야합니다. x86 ISA는 자연적으로 10 바이트의로드 / 저장소를 지원하지 않으므로 잠금 ( cmpxchg16b
) 없이 생각할 수있는 유일한 방법 에는 64 비트 모드가 필요 하기 때문에 실제로 비효율적 입니다.
에서 -O0
부품을 임시 변수에 할당하여 큰 표현을 어기면 더 많은 저장 / 재로드가 발생합니다. volatile
그렇지 않으면 , 실제 코드의 실제 빌드가 사용하는 최적화 설정과 관련이 없습니다.
C 앨리어싱 규칙을 사용하면 a char
가 별칭을 지정할 수 있으므로 char*
컴파일러를 통해 저장 하면 바이트 저장소 전후에 모든 위치를 저장 / 재로드해야 -O3
합니다. (이것은 예를 들어 의 배열에서 작동하는uint8_t
자동 벡터화 코드 의 문제입니다 .)
시도 uint16_t
아마도 16 비트 피연산자 크기 (잠재적 노점) 및 / 또는 추가 사용하여 16 비트에 절단을 강제로, 루프 카운터를 movzx
지침 (안전). 서명 오버 플로우가 정의되지 않은 동작입니다 사용 그렇게하지 않는 한, -fwrapv
또는 적어도 -fno-strict-overflow
, 서명 루프 카운터마다 반복을 다시 서명-확장 될 필요가 없습니다 64 비트 포인터 오프셋으로 사용되는 경우에도.
정수에서 float
다시 다시 강제로 변환 합니다. 및 / 또는 double
<=> float
변환. 명령어는 1보다 큰 대기 시간을 가지며 스칼라 int-> float ( cvtsi2ss
)는 나머지 xmm 레지스터를 0으로 만들지 않도록 잘못 설계되었습니다. (gcc pxor
는 이런 이유로 의존성을 깨뜨리기 위해 여분 을 삽입합니다 .)
CPU 선호도를 다른 CPU (@Egwor가 제안한)로 자주 설정하십시오 . 악마의 추론 : 하나의 코어가 오랫동안 스레드를 작동시켜 과열되는 것을 원하지 않습니다. 다른 코어로 교체하면 해당 코어가 더 높은 클럭 속도로 전환 될 수 있습니다. (실제로는 서로 열이 너무 가까워 멀티 소켓 시스템을 제외하고는 거의 불가능합니다). 이제 튜닝을 잘못하고 너무 자주 수행하십시오. OS 저장 / 복원 스레드 상태에서 소비 한 시간 외에 새로운 코어에는 콜드 L2 / L1 캐시, uop 캐시 및 분기 예측기가 있습니다.
불필요하게 자주 시스템을 호출하면 시스템 속도에 관계없이 속도가 느려질 수 있습니다. 비록 중요하지만 간단한 것들이 gettimeofday
커널 모드로 전환하지 않고 사용자 공간에서 구현 될 수 있습니다. (Linux에서 glibc는 커널이의 코드를 내보내므로 커널의 도움으로이를 수행합니다 vdso
).
시스템 호출 오버 헤드 (컨텍스트 스위치 자체가 아니라 사용자 공간으로 돌아온 후 캐시 / TLB 누락 포함)에 대한 자세한 내용을 위해 FlexSC 백서 에는 현재 상황에 대한 훌륭한 성능 카운터 분석과 배치 시스템 제안이 있습니다. 대규모 멀티 스레드 서버 프로세스의 호출
while(true){}