Xeon CPU(E5-2603)向后内存预取

时间:2018-08-20 15:08:03

标签: performance caching optimization x86 intel

向后内存预取是否与Xeon CPU(E5-2603)中的正向内存预取一样快?

我想实现一种算法,该算法需要对数据进行正向循环和反向循环。

由于每次迭代都需要最后一次迭代的结果,所以我无法反转循环的顺序。

谢谢。

1 个答案:

答案 0 :(得分:3)

您可以运行实验以确定数据预取器是否能够处理前向顺序访问和后向顺序访问。我有一个Haswell CPU,因此预取器可能与您的CPU(Sandy Bridge)中实现的预取器不同。

下图显示了以四种不同方式遍历数组时每个元素的访问可观察到的延迟:

  • 该数组在向前方向上顺序初始化,然后以相同方式遍历。我将此模式称为forfor
  • 在向前方向上依次初始化数组,然后在向后方向上(从最后一个元素到第一个元素)依次遍历。我将此模式称为forback
  • 该数组在向后的方向上顺序初始化,然后以相同的方式遍历。我将此模式称为backback

x轴表示元素索引,y轴表示TSC周期中的延迟。我已经配置了系统,以便TSC周期大约等于核心周期。我已经绘制了两次运行forforforfor1的{​​{1}}的测量值。平均每个元素的延迟如下:

  • forfor2:9.9个周期。
  • forfor1:15个周期。
  • forfor2:35.8个周期。
  • forback:40.3个周期。
L1访问延迟对任何测量噪声特别敏感。 L2访问延迟应该平均为12 cycles,但是由于L1命中次数少,我们可能仍会获得12个周期的延迟。在backback的第一轮中,大多数延迟为4个周期,这清楚地表明L1命中。在forfor的第二次运行中,大多数延迟为8或12个周期。我认为这些也可能是L1热门歌曲。在这两种情况下,都有一些L3命中和很少的主内存访问。对于forforforback,我们可以看到大多数延迟是L3命中。这意味着L3预取器能够处理向前和向后遍历,而L1和L2预取器则不能。

但是,访问是一个接一个地快速连续地执行的,而它们之间基本上没有计算。因此,如果L2预取器确实尝试向后预取,则可能会使数据太迟,因此仍会产生类似L3的延迟。

请注意,我没有在数组的两次遍历之间刷新缓存,因此第一次遍历可能会影响在第二次遍历中测得的延迟。

enter image description here

这是我用来进行测量的代码。

backback

这些实验的目的是测量各个访问延迟,以确定每个访问从哪个缓存级别进行服务。但是,由于存在/* compile with gcc at optimization level -O3 */ /* set the minimum and maximum CPU frequency for all cores using cpupower to get meaningful results */ /* run using "sudo nice -n -20 ./a.out" to minimize possible context switches, or at least use "taskset -c 0 ./a.out" */ /* make sure all cache prefetchers are enabled */ /* preferrably disable HT */ /* this code is Intel-specific */ /* see the note at the end of the answer */ #include <stdint.h> #include <x86intrin.h> #include <stdio.h> // 2048 iterations. #define LINES_SIZE 64 #define ITERATIONS 2048 * LINES_SIZE // Forward #define START 0 #define END ITERATIONS // Backward //#define START ITERATIONS - LINES_SIZE //#define END 0 #if START < END #define INCREMENT i = i + LINES_SIZE #define COMP < #else #define INCREMENT i = i - LINES_SIZE #define COMP >= #endif int main() { int array[ ITERATIONS ]; int latency[ ITERATIONS/LINES_SIZE ]; uint64_t time1, time2, al, osl; /* initial values don't matter */ // Perhaps necessary to prevents UB? for ( int i = 0; i < ITERATIONS; i = i + LINES_SIZE ) { array[ i ] = i; } printf( "address = %p \n", &array[ 0 ] ); /* guaranteed to be aligned within a single cache line */ // Measure overhead. _mm_mfence(); _mm_lfence(); /* mfence and lfence must be in this order + compiler barrier for rdtsc */ time1 = __rdtsc(); /* set timer */ _mm_lfence(); /* serialize rdtsc with respect to trailing instructions + compiler barrier for rdtsc */ /* no need for mfence because there are no stores in between */ _mm_lfence(); /* mfence and lfence must be in this order + compiler barrier for rdtsc */ time2 = __rdtsc(); _mm_lfence(); /* serialize rdtsc with respect to trailing instructions */ osl = time2 - time1; // Forward or backward traversal. for ( int i = START; i COMP END; INCREMENT ) { _mm_mfence(); /* this properly orders both clflush and rdtsc */ _mm_lfence(); /* mfence and lfence must be in this order + compiler barrier for rdtsc */ time1 = __rdtsc(); /* set timer */ _mm_lfence(); /* serialize rdtsc with respect to trailing instructions + compiler barrier for rdtsc */ int temp = array[ i ]; /* access array[i] */ _mm_lfence(); /* mfence and lfence must be in this order + compiler barrier for rdtsc */ time2 = __rdtsc(); _mm_lfence(); /* serialize rdtsc with respect to trailing instructions */ al = time2 - time1; printf( "array[ %i ] = %i \n", i, temp ); /* prevent the compiler from optimizing the load */ latency[i/64] = al - osl; } // Output measured latencies. for ( int i = 0; i < ITERATIONS/LINES_SIZE; ++i ) { printf( "%i \n", latency[i] ); } return 0; } 指令,因此测量结果可能包括加载指令在流水线其他阶段所需的延迟。另外,编译器将一些ALU指令放置在定时区域中,因此测量可能会受到这些指令的影响(可以通过在汇编中编写代码来避免这种情况)。这可能很难区分在L1中命中的访问和在L2中命中的访问。例如,某些L1等待时间测量报告为8个周期。尽管如此,LFENCEforback的测量结果清楚地表明,大多数访问都在L3中进行。

如果我们对测量访问特定级别的内存层次结构的平均延迟感兴趣,那么使用指针追踪可以提供更准确的结果。实际上,这是测量内存延迟的传统方法。

如果您以某种方式难以访问硬件预取器(尤其是L2或L3的预取器)来预测大量数据,则软件预取会非常有益。但是,通常很难正确地进行软件预取。另外,我得到的测量结果表明,L3预取器可以向前和向后预取。如果在内存访问和计算方面都具有足够的并行性,那么OoO执行可以掩盖L3访问延迟的很大一部分。


正确运行程序的重要提示:事实证明,如果我不使用输出重定向运算符>将所有输出重定向到文件,即所有输出将打印在在终端上,所有测得的延迟将接近L3命中延迟。这样做的原因是在每次迭代中都被调用的backback正在污染很多L1和L2缓存。因此,请确保使用>运算符。您也可以使用printf代替thisthis答案中建议的(void) *((volatile int*)array + i)。那会更加可靠。