使用预取加速随机内存访问

时间:2016-12-03 17:04:09

标签: performance gcc compiler-optimization cpu-cache prefetch

我正在尝试使用预取来加速单个程序。我的程序的目的只是为了测试。这是它的作用:

  
      
  1. 它使用两个相同大小的int缓冲区
  2.   
  3. 它逐个读取第一个缓冲区的所有值
  4.   
  5. 它读取第二个缓冲区中索引处的值
  6.   
  7. 它汇总了从第二个缓冲区中获取的所有值
  8.   
  9. 它为更大更大的
  10. 做了所有前面的步骤   
  11. 最后,我打印了自愿和非自愿CPU的数量
  12.   

第一次,第一个缓冲区中的值包含其索引的值(参见下面代码中的函数createIndexBuffer)。

我的程序代码会更清楚:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/time.h>

#define BUFFER_SIZE ((unsigned long) 4096 * 100000)


unsigned int randomUint()
{
  int value = rand() % UINT_MAX;
  return value;
}


unsigned int * createValueBuffer()
{
  unsigned int * valueBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int));
  for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++)
  {
    valueBuffer[i] = randomUint();
  }

  return (valueBuffer);
}


unsigned int * createIndexBuffer()
{
  unsigned int * indexBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int));
  for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++)
  {
    indexBuffer[i] = i;
  }

  return (indexBuffer);
}


unsigned long long computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer)
{
  unsigned long long sum = 0;

  for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++)
  {
    unsigned int index = indexBuffer[i];
    sum += valueBuffer[index];
  }

  return (sum);
}


unsigned int computeTimeInMicroSeconds()
{
  unsigned int * valueBuffer = createValueBuffer();
  unsigned int * indexBuffer = createIndexBuffer();

  struct timeval startTime, endTime;
  gettimeofday(&startTime, NULL);

  unsigned long long sum = computeSum(indexBuffer, valueBuffer);

  gettimeofday(&endTime, NULL);

  printf("Sum = %llu\n", sum);
  free(indexBuffer);
  free(valueBuffer);

  return ((endTime.tv_sec - startTime.tv_sec) * 1000 * 1000) + (endTime.tv_usec - startTime.tv_usec);

}


int main()
{
  printf("sizeof buffers = %ldMb\n", BUFFER_SIZE * sizeof(unsigned int) / (1024 * 1024));
  unsigned int timeInMicroSeconds = computeTimeInMicroSeconds();
  printf("Time: %u micro-seconds = %.3f seconds\n", timeInMicroSeconds, (double) timeInMicroSeconds / (1000 * 1000));
}

如果我启动它,我会得到以下输出:

$ gcc TestPrefetch.c -O3 -o TestPrefetch && ./TestPrefetch 
sizeof buffers = 1562Mb
Sum = 439813150288855829
Time: 201172 micro-seconds = 0.201 seconds

快速而快速! 根据我的知识(我可能错了),拥有如此快速程序的原因之一是,当我按顺序访​​问我的两个缓冲区时,可以在CPU缓存中预取数据。

我们可以使它更复杂,以便(几乎)在CPU缓存中预先输入数据。例如,我们只需更改createIndexBuffer函数:

unsigned int * createIndexBuffer()
{
  unsigned int * indexBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int));
  for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++)
  {
    indexBuffer[i] = rand() % BUFFER_SIZE;
  }

  return (indexBuffer);
}

让我们再试一次这个程序:

$ gcc TestPrefetch.c -O3 -o TestPrefetch && ./TestPrefetch 
sizeof buffers = 1562Mb
Sum = 439835307963131237
Time: 3730387 micro-seconds = 3.730 seconds

慢了18倍!!!

我们现在遇到了我的问题。鉴于新的createIndexBuffer函数,我想使用预取加速computeSum函数

unsigned long long computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer)
{
  unsigned long long sum = 0;

  for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++)
  {
    __builtin_prefetch((char *) &indexBuffer[i + 1], 0, 0);
    unsigned int index = indexBuffer[i];
    sum += valueBuffer[index];
  }

  return (sum);
}

当然我还必须更改我的createIndexBuffer,以便分配一个具有一个元素的缓冲区

我重新启动了我的计划:不是更好!由于预取可能比一个“for”循环迭代慢,我可能先预取一个元素,但之前只有两个元素

    __builtin_prefetch((char *) &indexBuffer[i + 2], 0, 0);

不是更好!两个循环迭代? 不是更好?三? **我试过它直到50(!!!)但我无法提高我的函数computeSum的性能。

我想帮助理解原因吗? 非常感谢你的帮助

4 个答案:

答案 0 :(得分:3)

我相信上面的代码会由CPU自动优化,而不需要任何进一步的手动优化空间。

1。主要问题是按顺序访问indexBuffer。硬件预取器会感知它并自动预取更多值,而无需手动调用预取。因此,在迭代#i期间,值indexBuffer[i+1]indexBuffer[i+2],...已经在缓存中。 (顺便说一句,没有必要在数组末尾添加人工元素:预取指令会默默忽略内存访问错误。)

您真正需要做的是预取valueBuffer而不是:

__builtin_prefetch((char *) &valueBuffer[indexBuffer[i + 1]], 0, 0);

2. 但是,在这种简单的情况下,添加上面的代码行也无济于事。访问内存的成本是几百个周期,而添加指令是~1个周期。您的代码已经花了99%的时间进行内存访问。添加手动预取将使这一周期更快,而且没有更好。

如果你的数学更重(尝试一下),手动预取真的会很好用,比如使用一个带有大量非优化输出除法的表达式(每个20-30个循环)或调用一些数学函数(log,sin) )。

3. 但即便如此也无法保证提供帮助。循环迭代之间的依赖性非常弱,只能通过sum变量。这允许CPU以推测方式执行指令:它可以在仍然执行valueBuffer[i+1]的数学运算的同时开始同时获取valueBuffer[i]

答案 1 :(得分:0)

对不起。我给你的不是我的代码的正确版本。你说的是正确的版本:

__builtin_prefetch((char *) &valueBuffer[indexBuffer[i + prefetchStep]], 0, 0);

然而,即使使用正确的版本,遗憾的是不是更好

答案 2 :(得分:0)

然后我调整了我的程序,使用sin函数来尝试你的建议。

我改编的计划如下:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/time.h>
#include <math.h>

#define BUFFER_SIZE ((unsigned long) 4096 * 50000)


unsigned int randomUint()
{
  int value = rand() % UINT_MAX;
  return value;
}


unsigned int * createValueBuffer()
{
  unsigned int * valueBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int));
  for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++)
  {
    valueBuffer[i] = randomUint();
  }

  return (valueBuffer);
}


unsigned int * createIndexBuffer(unsigned short prefetchStep)
{
  unsigned int * indexBuffer = (unsigned int *) malloc((BUFFER_SIZE + prefetchStep) * sizeof(unsigned int));
  for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++)
  {
    indexBuffer[i] = rand() % BUFFER_SIZE;
  }

  return (indexBuffer);
}


double computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer, unsigned short prefetchStep)
{
  double sum = 0;

  for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++)
  {
    __builtin_prefetch((char *) &valueBuffer[indexBuffer[i + prefetchStep]], 0, 0);
    unsigned int index = indexBuffer[i];
    sum += sin(valueBuffer[index]);
  }

  return (sum);
}


unsigned int computeTimeInMicroSeconds(unsigned short prefetchStep)
{
  unsigned int * valueBuffer = createValueBuffer();
  unsigned int * indexBuffer = createIndexBuffer(prefetchStep);

  struct timeval startTime, endTime;
  gettimeofday(&startTime, NULL);

  double sum = computeSum(indexBuffer, valueBuffer, prefetchStep);

  gettimeofday(&endTime, NULL);

  printf("prefetchStep = %d, Sum = %f - ", prefetchStep, sum);
  free(indexBuffer);
  free(valueBuffer);

  return ((endTime.tv_sec - startTime.tv_sec) * 1000 * 1000) + (endTime.tv_usec - startTime.tv_usec);

}


int main()
{
  printf("sizeof buffers = %ldMb\n", BUFFER_SIZE * sizeof(unsigned int) / (1024 * 1024));
  for (unsigned short prefetchStep = 0 ; prefetchStep < 250 ; prefetchStep++)
  {
    unsigned int timeInMicroSeconds = computeTimeInMicroSeconds(prefetchStep);
    printf("Time: %u micro-seconds = %.3f seconds\n", timeInMicroSeconds, (double) timeInMicroSeconds / (1000 * 1000));
  }
}

输出结果为:

$ gcc TestPrefetch.c -O3 -o TestPrefetch -lm && taskset -c 7 ./TestPrefetch 
sizeof buffers = 781Mb
prefetchStep = 0, Sum = -1107.523504 - Time: 20895326 micro-seconds = 20.895 seconds
prefetchStep = 1, Sum = 13456.262424 - Time: 12706720 micro-seconds = 12.707 seconds
prefetchStep = 2, Sum = -20179.289469 - Time: 12136174 micro-seconds = 12.136 seconds
prefetchStep = 3, Sum = 12068.302534 - Time: 11233803 micro-seconds = 11.234 seconds
prefetchStep = 4, Sum = 21071.238160 - Time: 10855348 micro-seconds = 10.855 seconds
prefetchStep = 5, Sum = -22648.280105 - Time: 10517861 micro-seconds = 10.518 seconds
prefetchStep = 6, Sum = 22665.381676 - Time: 9205809 micro-seconds = 9.206 seconds
prefetchStep = 7, Sum = 2461.741268 - Time: 11391088 micro-seconds = 11.391 seconds
...

所以在这里,它更好!老实说,我几乎可以肯定它不会更好,因为与内存访问相比,数学函数成本更高。

如果有人能给我更多关于为什么它现在变得更好的信息,我将不胜感激

非常感谢

答案 3 :(得分:0)

预取通常会获取完整的缓存行。这是typically 64 bytes。因此随机示例总是为4字节int提取64个字节。实际需要的数据的16倍,这与减速18倍非常吻合。因此,代码仅受内存吞吐量的限制而非延迟。