Intel Sandybridge 제품군 CPU의 파이프 라인에 대한 프로그램 최적화 해제


322

나는이 과제를 완수하기 위해 일주일 동안 내 두뇌를 쌓아 왔고 여기 누군가가 나를 올바른 길로 인도 할 수 있기를 바라고 있습니다. 강사의 지시로 시작하겠습니다.

귀하의 과제는 소수 프로그램을 최적화하기위한 첫 번째 실험실 과제와 반대입니다. 이 과제의 목적은 프로그램을 비관 화하는 것, 즉 프로그램을 느리게하는 것입니다. 이 두 가지 모두 CPU를 많이 사용하는 프로그램입니다. 랩 PC에서 실행하는 데 몇 초가 걸립니다. 알고리즘을 변경할 수 없습니다.

프로그램을 최적화 해제하려면 Intel i7 파이프 라인 작동 방식에 대한 지식을 사용하십시오. WAR, RAW 및 기타 위험 요소를 도입하기 위해 명령 경로를 재정렬하는 방법을 상상해보십시오. 캐시의 효율성을 최소화하는 방법을 생각하십시오. 비현실적으로 무능합니다.

과제는 Whetstone 또는 Monte-Carlo 프로그램을 선택했습니다. 캐시 효과 설명은 대부분 Whetstone에만 적용 할 수 있지만 Monte-Carlo 시뮬레이션 프로그램을 선택했습니다.

// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm>    // Needed for the "max" function
#include <cmath>
#include <iostream>

// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in 
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
  double x = 0.0;
  double y = 0.0;
  double euclid_sq = 0.0;

  // Continue generating two uniform random variables
  // until the square of their "euclidean distance" 
  // is less than unity
  do {
    x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    euclid_sq = x*x + y*y;
  } while (euclid_sq >= 1.0);

  return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}

// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(S_cur - K, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

// Pricing a European vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(K - S_cur, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

int main(int argc, char **argv) {
  // First we create the parameter list                                                                               
  int num_sims = 10000000;   // Number of simulated asset paths                                                       
  double S = 100.0;  // Option price                                                                                  
  double K = 100.0;  // Strike price                                                                                  
  double r = 0.05;   // Risk-free rate (5%)                                                                           
  double v = 0.2;    // Volatility of the underlying (20%)                                                            
  double T = 1.0;    // One year until expiry                                                                         

  // Then we calculate the call/put values via Monte Carlo                                                                          
  double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
  double put = monte_carlo_put_price(num_sims, S, K, r, v, T);

  // Finally we output the parameters and prices                                                                      
  std::cout << "Number of Paths: " << num_sims << std::endl;
  std::cout << "Underlying:      " << S << std::endl;
  std::cout << "Strike:          " << K << std::endl;
  std::cout << "Risk-Free Rate:  " << r << std::endl;
  std::cout << "Volatility:      " << v << std::endl;
  std::cout << "Maturity:        " << T << std::endl;

  std::cout << "Call Price:      " << call << std::endl;
  std::cout << "Put Price:       " << put << std::endl;

  return 0;
}

변경 사항으로 인해 코드 실행 시간이 1 초 증가하는 것처럼 보였지만 코드를 추가하지 않고 파이프 라인을 중단시키기 위해 무엇을 변경할 수 있는지 완전히 확신하지 못했습니다. 올바른 방향으로 향하는 점은 굉장 할 것입니다. 나는 모든 답변에 감사드립니다.


업데이트 : 이 과제를 한 교수가 세부 사항을 게시했습니다.

주요 내용은 다음과 같습니다.

  • 커뮤니티 칼리지에서 두 번째 학기 건축 수업입니다 (Hennesy 및 Patterson 교재 사용).
  • 랩 컴퓨터에는 Haswell CPU가 있습니다
  • 학생들은 CPUID본질과 CLFLUSH수업 뿐만 아니라 수업 시간과 캐시 크기를 결정하는 방법 에 노출되었습니다 .
  • 모든 컴파일러 옵션이 허용되며 인라인 asm도 허용됩니다.
  • 자신의 제곱근 알고리즘 작성은 창백한 외부에 있다고 발표되었습니다.

메타 스레드에 대한 Cowmoogun의 의견은 컴파일러 최적화가 이것의 일부가 될 수 있음이 분명하지 않다고 가정-O0 하고 런타임의 17 % 증가가 합리적이라고 지적했습니다.

따라서 과제의 목표는 학생들이 수업 수준의 병렬 처리 또는 이와 유사한 것을 줄이기 위해 기존 작업을 재정렬하는 것이었지만 사람들이 더 깊이 탐구하고 더 많이 배우는 것은 나쁜 것이 아닙니다.


이것은 C ++을 일반적으로 느리게 만드는 방법에 대한 질문이 아니라 컴퓨터 아키텍처 질문이라는 점을 명심하십시오.


97
i7이 다음과 같이 매우 열악하다고 들었습니다.while(true){}
Cliff AB


5
openmp를 사용하면 N 스레드를 1보다 길게 만들 수 있어야합니다.
Flexo

9
이 질문은 이제 메타
Madara의 유령

3
@ bluefeet : 나는 다시 열린 후 한 시간 안에 이미 하나의 투표권을 끌어 들였기 때문에 덧붙였다. 메타에 대해 논의중인 의견을 읽지 않으면 서 5 명만 참여하고 VTC 만 있으면됩니다. 또 다른 찬성 투표가 있습니다. 적어도 한 문장은 닫기 / 다시 열기주기를 피하는 데 도움이된다고 생각합니다.
Peter Cordes

답변:


405

중요한 배경 읽기 : Agner Fog의 microarch pdf 및 아마도 모든 프로그래머가 메모리에 대해 알아야 할 Ulrich Drepper도 있습니다 . 의 다른 링크도 참조하십시오태그 위키, 특히 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변수를 루프 카운터로 사용하는 것을 의미 합니다. 이것은 atomicfor 루프 카운터의 사용을 정당화 하고 실제 캐시 라인 핑퐁을 생성합니다 (스레드가 하이퍼 스레딩을 사용하여 동일한 물리적 코어에서 실행되지 않는 느리지 는 않습니다 ). 어쨌든 이것은에 대한 비경쟁 사례보다 훨씬 느립니다 lock inc. 그리고 32 비트 시스템에 대한 문제 lock cmpxchg8b를 원자 적으로 증가 uint64_t시키기 위해서는 하드웨어가 원자를 중재하는 대신 루프에서 다시 시도해야합니다 inc.

또한 여러 스레드가 동일한 캐시 라인의 다른 바이트에 개인 데이터 (예 : RNG 상태)를 유지하는 허위 공유를 만듭니다 . (perf 카운터를 포함하여 그것에 관한 인텔 튜토리얼) . 여기에는 마이크로 아키텍처 관련 측면이 있습니다 . 인텔 CPU 는 메모리 오더 가 발생 하지 않는 것으로 추측하고 , 적어도 P4에서이를 감지하기 위한 메모리 순서 시스템 클리어 퍼프 이벤트가 있습니다. 벌금은 Haswell에서 크지 않을 수 있습니다. 그 링크가 지적한 바와 같이, locked 명령어는 잘못 추측하지 않고, 이것이 일어날 것이라고 가정합니다. 일반로드는 다른 코어가로드가 실행될 때와 프로그램 순서에서 종료 될 때 사이에 캐시 라인을 무효화하지 않을 것이라고 추측합니다 (를 사용하지 않는 한pause ). locked 지침이 없는 진정한 공유 는 일반적으로 버그입니다. 원자가 아닌 공유 루프 카운터를 원자 사례와 비교하는 것이 흥미로울 것입니다. 실제로 비관하려면 공유 원자 루프 카운터를 유지하고 다른 변수에 대해 동일하거나 다른 캐시 라인에서 잘못된 공유를 유발하십시오.


임의의 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 xmmNehalem (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 벡터와 호출 expvzeroupper먼저 사용 하지 않으면 중단 됩니다. 돌아온 후, 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는 movzx8 또는 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=intel32 비트 명령어를 사용했을 때나 사용할 수있는 경우 에도 그러한 명령어를 생성 합니다.


타이밍에 대한 일반적인 관용구는 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을 물리치게 됩니다 . #defines를 사용하여 간단한 스칼라 변수를로 바꿉니다 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 및 locked 명령어는 다른 스레드의 경합이 없어도 상당히 느립니다.

-m32x87 코드가 SSE2 코드보다 나쁘기 때문에 코드 속도가 느려집니다. 스택 기반 32 비트 호출 규칙은 더 많은 명령어를 사용하며 스택의 FP 인수도 같은 함수에 전달 exp()합니다. atomic<uint64_t>::operator++on -m32에는 lock cmpxchg8B루프 (i586) 가 필요합니다 . (따라서 루프 카운터에 사용하십시오! [악한 웃음]).

-march=i386또한 비관적입니다 (@Jesper 덕분에). FP는 fcom686보다 느립니다 fcomi. Pre-586은 원자 64 비트 저장소 (cmpxchg 제외)를 제공하지 않으므로 모든 64 비트 atomicop는 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 백서 에는 현재 상황에 대한 훌륭한 성능 카운터 분석과 배치 시스템 제안이 있습니다. 대규모 멀티 스레드 서버 프로세스의 호출


10
@JesperJuhl : 예, 그 정당성을 사겠습니다. "비주류 적으로 무능한"은 훌륭한 문구입니다 :)
Peter Cordes

2
상수의 곱셈을 상수의 역수로 나눈 값을 변경하면 성능이 약간 저하 될 수 있습니다 (적어도 -O3 -fastmath를 능가하려고하지 않는 경우). 비슷하게 작동을 증가 연관성을 사용하여 ( exp(T*(r-0.5*v*v))이되고 exp(T*r - T*v*v/2.0), exp(sqrt(v*v*T)*gauss_bm)되고 exp(sqrt(v)*sqrt(v)*sqrt(T)*gauss_bm)). 연관성 (및 일반화)은 exp(T*r - T*v*v/2.0)`pow ((pow (e_value, T), r) / pow (pow (pow ((pow (e_value, T), v), v), v)),-2.0) [또는 무엇인가로 변환 될 수 있습니다. 그러한 수학 트릭은 실제로 미시건 축적 최적화 해제로 간주되지 않습니다
Paul A. Clayton

2
이 답변에 감사 드리며 Agner 's Fog는 큰 도움이되었습니다. 이 다이제스트를 오늘 오후에 시작하도록하겠습니다. 이것은 실제로 무슨 일이 일어나고 있는지 배우는 측면에서 가장 유용한 과제였습니다.
Cowmoogun

19
이러한 제안 중 일부는 매우 대사 력이 부족하여 교수님과 상담하여 현재 7 분의 달리기 시간이 너무 길어서 출력을 확인하기를 원치 않는지 확인해야합니다. 여전히이 작업을 수행하면 프로젝트에서 가장 재미 있었을 것입니다.
Cowmoogun

4
뭐? 뮤텍스가 없습니까? 뮤텍스와 동시에 2 백만 개의 스레드를 실행하면 각각의 개별 계산을 보호 할 수 있습니다 (만약의 경우!). 즉, 나는이 대사 적으로 무능한 대답을 좋아합니다.
David Hammen

35

가능한 한 나쁜 일을하도록하기 위해 할 수있는 몇 가지 :

  • i386 아키텍처를위한 코드를 컴파일하십시오. 이렇게하면 SSE 및 최신 지침을 사용하지 못하고 x87 FPU를 강제로 사용하게됩니다.

  • std::atomic어디에서나 변수를 사용 하십시오. 이것은 컴파일러가 모든 곳에서 메모리 장벽을 삽입해야하기 때문에 매우 비쌉니다. 그리고 이것은 무능한 사람이 "실 안전을 보장"하기 위해 그럴듯하게 할 수있는 일입니다.

  • 프리 페 처가 예측할 수있는 최악의 방식으로 메모리에 액세스해야합니다 (열 전공 대 행 전공).

  • 변수를 더 비싸게 만들려면 변수에 new'자동 저장 기간'(스택 할당)을 허용하지 않고 할당하여 모두 '동적 저장 기간'(힙 할당)을 가질 수 있습니다.

  • 할당하는 모든 메모리가 매우 이상하게 정렬되고 너무 큰 페이지를 할당하지 않는 것이 좋습니다. 그렇게하면 TLB가 너무 효율적이기 때문입니다.

  • 컴파일러 옵티마이 저가 활성화 된 상태에서 코드를 작성하지 마십시오. 또한 가장 표현력이 뛰어난 디버그 기호를 활성화해야합니다 (코드 느리게 실행 되지는 않지만 추가 디스크 공간이 낭비 됨).

참고 :이 답변은 기본적으로 @Peter Cordes가 이미 그의 훌륭한 답변에 통합 한 내 의견을 요약합니다. 당신이 여분의 것을 하나만 가지고 있다면 그가 당신의 투표를 제안하십시오 :)


9
이들 중 일부 내 주요 이의 질문의 말씨 있습니다 : 프로그램을 deoptimize하려면, 인텔 I7 파이프 라인의 작동 방식에 대한 지식을 사용합니다 . 나는 x87에 대한 uarch-specific, 또는 std::atomic동적 할당으로부터의 추가적인 간접적 인 수준이 있다고 생각하지 않습니다 . Atom 또는 K8에서도 느려질 것입니다. 여전히 공의를 표하기는했지만, 나는 당신의 제안 중 일부를 거부했습니다.
Peter Cordes

그것들은 공정한 포인트입니다. 그럼에도 불구하고, 그러한 것들은 여전히 ​​공포 자의 목표를 향해 작동합니다. 공감대 감사합니다 :)
Jesper Juhl

SSE 장치는 포트 0, 1 및 5를 사용합니다. x87 장치는 포트 0과 1 만 사용합니다.
Michas

@ 마이클 : 당신은 그것에 대해 잘못입니다. Haswell은 포트 5에서 SSE FP 수학 명령을 실행하지 않습니다. 대부분 SSE FP 셔플 및 부울 (xorps / andps / orps). x87은 느리지 만 왜 약간 잘못된 지에 대한 설명입니다. (그리고이 점은 완전히 잘못된 것입니다.)
피터 코르

1
@Michas : movapd xmm, xmm일반적으로 실행 포트가 필요하지 않습니다 (IVB 이상의 레지스터 이름 변경 단계에서 처리됨). FMA 이외의 것은 비파괴 적이므로 AVX 코드에서는 거의 필요하지 않습니다. 그러나 공정하게도 Haswell은 포트 5를 제거하지 않으면 포트 5에서 실행합니다. x87 register-copy ( fld st(i))를 보지 않았지만 Haswell / Broadwell은 옳습니다 : p01에서 실행됩니다. Skylake는 p05에서 실행하고 SnB는 p0에서 실행하고 IvB는 p5에서 실행합니다. 따라서 IVB / SKL은 p5에서 x87 작업 (비교 포함)을 수행하지만 SNB / HSW / BDW는 x87에 p5를 전혀 사용하지 않습니다.
피터 코 데스

11

long double계산에 사용할 수 있습니다 . x86에서는 80 비트 형식이어야합니다. 레거시 x87 FPU 만 지원합니다.

x87 FPU의 몇 가지 단점 :

  1. SIMD가 부족합니다. 추가 지침이 필요할 수 있습니다.
  2. 스택 기반이며 수퍼 스칼라 및 파이프 라인 아키텍처에 문제가 있습니다.
  3. 별개의 아주 작은 레지스터 세트는 다른 레지스터에서 더 많은 변환과 더 많은 메모리 작업이 필요할 수 있습니다.
  4. Core i7에는 SSE 용 포트 3 개와 x87 용 포트 2 개가 있으며 프로세서는 병렬 명령을 덜 실행할 수 있습니다.

3
스칼라 수학의 경우 x87 수학 명령어 자체는 약간 느립니다. 그러나 10 바이트 피연산자의 저장 /로드는 상당히 느리며 x87의 스택 기반 디자인에는 추가 명령어 (예 :)가 필요한 경향이 fxch있습니다. 와 -ffast-math좋은 컴파일러는하지만, 몬테 - 카를로 루프를 벡터화 수 있으며 x87는 것을 방지합니다.
Peter Cordes

나는 대답을 조금 확장했다.
Michas

1
re : 4 : 어떤 i7 uarch에 대해 이야기하고 있습니까? Haswell은 mulssp01에서 실행할 수 있지만 fmul에서만 실행할 수 있습니다 p0. addss에서 p1와 동일 하게 실행 됩니다 fadd. FP 수학 연산을 처리하는 실행 포트는 두 개뿐입니다. (이에 대한 유일한 예외는 Skylake가 전용 추가 장치를 삭제하고 addssp01에서 pMA의 FMA 장치에서 실행 하지만 faddp5 에서 실행 한다는 것 입니다. 따라서 fadd와 함께 일부 명령어를 혼합 fma...ps하면 이론적으로 총 FLOP / 조각을 약간 더 높일 수 있습니다.)
Peter Cordes

2
또한 Windows x86-64 ABI에는 64 비트가 있습니다 long double. 즉, 여전히 그렇습니다 double. 그러나 SysV ABI는 80 비트를 사용 long double합니다. 또한 re : 2 : 레지스터 이름을 바꾸면 스택 레지스터의 병렬 처리가 나타납니다. 스택 기반 아키텍처에는 fxchgesp 와 같은 몇 가지 추가 지침이 필요합니다 . 병렬 계산을 인터리브 할 때. 따라서 메모리를 사용하지 않고 병렬 처리를 표현하는 것이 어렵다는 것입니다. 하지만 다른 정규직에서 더 이상 전환 할 필요는 없습니다. 그게 무슨 뜻인지 잘 모르겠습니다.
Peter Cordes

6

늦은 답변이지만 링크 목록과 TLB를 충분히 남용했다고 생각하지 않습니다.

mmap을 사용하여 대부분 주소의 MSB를 사용하도록 노드를 할당하십시오. 이로 인해 긴 TLB 조회 체인이 생성되고, 페이지는 12 비트이며, 변환을 위해 52 비트를 남겨 두거나 매번 통과해야하는 약 5 단계가 필요합니다. 운이 좋으면 노드에 도달하기 위해 5 레벨 조회 + 1 메모리 액세스를 위해 매번 메모리로 이동해야합니다. 최상위 레벨은 캐시에있을 가능성이 높으므로 5 * 메모리 액세스를 기대할 수 있습니다. 다음 포인터를 읽으면 또 다른 3-4 변환 조회가 발생하도록 최악의 경계를 넘어서 노드를 배치하십시오. 또한 대량의 변환 조회로 인해 캐시가 완전히 손상 될 수 있습니다. 또한 가상 테이블의 크기로 인해 대부분의 사용자 데이터가 추가 시간 동안 디스크에 페이징 될 수 있습니다.

하나의 링크 된 목록에서 읽을 때는 매번 목록의 처음부터 읽어야 단일 번호를 읽는 데 최대 지연이 발생합니다.


x86-64 페이지 테이블은 48 비트 가상 주소에 대해 4 단계 깊이입니다. PTE에는 52 비트의 물리적 주소가 있습니다. 향후 CPU는 또 다른 9 비트의 가상 주소 공간 (57)에 대해 5 단계 페이지 테이블 기능을 지원합니다. 64 비트에서 가상 주소가 실제 주소 (52 비트 길이)와 비교하여 4 비트 (48 비트 길이)가 짧은 이유는 무엇입니까? . OS는 속도가 느리고 기본 주소 공간이 많이 필요하지 않으면 이점이 없기 때문에 기본적으로 활성화하지 않습니다.
Peter Cordes

하지만 네, 재미있는 생각입니다. 당신은 아마 사용할 수있는 mmap실제 RAM의 동일한 금액을 더 TLB 미스를 허용 (같은 내용) 동일한 물리적 페이지에 대한 다수의 가상 주소를 얻기 위해 파일이나 공유 메모리 영역에. 링크 된 목록 next이 상대적 오프셋 인 경우 +4096 * 1024마지막으로 다른 실제 페이지에 도달 할 때까지 동일한 페이지에 대한 일련의 매핑을 가질 수 있습니다 . 또는 L1d 캐시 적중을 피하기 위해 여러 페이지에 걸쳐 있습니다. page-walk 하드웨어 내에서 상위 레벨 PDE 캐싱이 있으므로 virt addr 공간에 퍼 뜨리십시오!
Peter Cordes

이전 주소에 오프셋을 추가하면 [ [reg+small_offset]어드레싱 모드 의 특별한 경우 ] 를 무효화하여로드 사용 대기 시간이 더 나빠집니다 ( base + offset이 base와 다른 페이지에있을 때 패널티가 있습니까? ); add64 비트 오프셋 의 메모리 소스 를 얻거나 같은로드 및 인덱싱 된 주소 지정 모드를 얻습니다 [reg+reg]. 또한 L2 TLB가 누락 된 후 어떻게됩니까?를 참조하십시오 . -페이지 이동은 SnB 제품군의 L1d 캐시를 통해 가져옵니다.
Peter Cordes
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.