AVX 标量运算要快得多

Posted

技术标签:

【中文标题】AVX 标量运算要快得多【英文标题】:AVX scalar operations are much faster 【发布时间】:2017-09-01 13:28:57 【问题描述】:

我测试了下面的简单函数

void mul(double *a, double *b) 
  for (int i = 0; i<N; i++) a[i] *= b[i];

具有非常大的数组,因此它受内存带宽限制。我使用的测试代码如下。当我使用-O2 编译时,需要 1.7 秒。当我使用-O2 -mavx 编译时,只需要 1.0 秒。非 vex 编码的标量运算要慢 70%! 这是为什么?

这是-O2-O2 -mavx 的程序集。

https://godbolt.org/g/w4p60f

系统:i7-6700HQ@2.60GHz (Skylake) 32 GB 内存,Ubuntu 16.10,GCC 6.3

测试代码

//gcc -O2 -fopenmp test.c
//or
//gcc -O2 -mavx -fopenmp test.c
#include <string.h>
#include <stdio.h>
#include <x86intrin.h>
#include <omp.h>

#define N 1000000
#define R 1000

void mul(double *a, double *b) 
  for (int i = 0; i<N; i++) a[i] *= b[i];


int main() 
  double *a = (double*)_mm_malloc(sizeof *a * N, 32);
  double *b = (double*)_mm_malloc(sizeof *b * N, 32);

  //b must be initialized to get the correct bandwidth!!!
  memset(a, 1, sizeof *a * N);
  memset(b, 1, sizeof *b * N);

  double dtime;
  const double mem = 3*sizeof(double)*N*R/1024/1024/1024;
  const double maxbw = 34.1;
  dtime = -omp_get_wtime();
  for(int i=0; i<R; i++) mul(a,b);
  dtime += omp_get_wtime();
  printf("time %.2f s, %.1f GB/s, efficency %.1f%%\n", dtime, mem/dtime, 100*mem/dtime/maxbw);

  _mm_free(a), _mm_free(b);

【问题讨论】:

我得走了。明天我会对此进行更多研究。 显然不仅 glibc 2.23 有时会返回脏的上层状态,而且 OpenMP 库也会返回。如果还有其他库在没有正确的vzeroupper 的情况下返回,我不会感到惊讶。只需在每次库调用后插入 vzeroupper,如果您希望 100% 确保在使用非 VEX 编码的 SSE 代码的 Skylake 上避免此问题。 肮脏的上层状态肯定会在 Skylake 而不是 Haswell 上解释这一点。在 Haswell 上,您只需支付一次大笔罚款即可进入分裂状态 - 然后您就可以全速奔跑。在 Skylake 上,您只需支付很少的转换损失,但您会被整个基准测试的错误依赖所困。 @wim,我猜omp_get_wtime() 调用gettimeofdate 或其他一些glibc 函数。我认为问题在于它第一次被调用时使用了 CPU 调度程序,这让它变得很脏。我只需要在第一次调用omp_get_wtime() 后使用vzeroupper 来解决问题。其他人在_dl_runtime_resolve_avx() 中发现了问题。对我来说,这看起来像是某种调度员。 A 可以通过 gdb(如果我能弄清楚如何使用它)来找出答案。 @wim omp_get_wtime 呼叫 clock_gettime。并且clock_gettime 调用_dl_runtime_resolve_avx。我的猜测是这就是问题所在。 【参考方案1】:

该问题与调用 omp_get_wtime() 后 AVX 寄存器的上半部分脏有关。这是 Skylake 处理器的一个问题。

我第一次读到这个问题是here。从那时起,其他人发现了这个问题:here 和 here。

使用gdb 我发现omp_get_wtime() 调用clock_gettime。我重写了我的代码以使用clock_gettime(),我看到了同样的问题。

void fix_avx()  __asm__ __volatile__ ( "vzeroupper" : : : ); 
void fix_sse()  
void (*fix)();

double get_wtime() 
  struct timespec time;
  clock_gettime(CLOCK_MONOTONIC, &time);
  #ifndef  __AVX__ 
  fix();
  #endif
  return time.tv_sec + 1E-9*time.tv_nsec;


void dispatch() 
  fix = fix_sse;
  #if defined(__INTEL_COMPILER)
  if (_may_i_use_cpu_feature (_FEATURE_AVX)) fix = fix_avx;
  #else
  #if defined(__GNUC__) && !defined(__clang__)
  __builtin_cpu_init();
  #endif
  if(__builtin_cpu_supports("avx")) fix = fix_avx;
  #endif

使用gdb 单步执行代码我看到第一次调用clock_gettime 时它调用_dl_runtime_resolve_avx()。我相信问题出在这个基于this comment 的函数上。这个函数似乎只在第一次调用 clock_gettime 时被调用。

使用 GCC,在第一次调用 clock_gettime 后使用 //__asm__ __volatile__ ( "vzeroupper" : : : ); 问题就消失了,但是使用 Clang(使用 clang -O2 -fno-vectorize,因为 Clang 向量化甚至在 -O2)它只会在每次调用 @987654342 后使用它才消失@。

这是我用来测试的代码(使用 GCC 6.3 和 Clang 3.8)

#include <string.h>
#include <stdio.h>
#include <x86intrin.h>
#include <time.h>

void fix_avx()  __asm__ __volatile__ ( "vzeroupper" : : : ); 
void fix_sse()  
void (*fix)();

double get_wtime() 
  struct timespec time;
  clock_gettime(CLOCK_MONOTONIC, &time);
  #ifndef  __AVX__ 
  fix();
  #endif
  return time.tv_sec + 1E-9*time.tv_nsec;


void dispatch() 
  fix = fix_sse;
  #if defined(__INTEL_COMPILER)
  if (_may_i_use_cpu_feature (_FEATURE_AVX)) fix = fix_avx;
  #else
  #if defined(__GNUC__) && !defined(__clang__)
  __builtin_cpu_init();
  #endif
  if(__builtin_cpu_supports("avx")) fix = fix_avx;
  #endif


#define N 1000000
#define R 1000

void mul(double *a, double *b) 
  for (int i = 0; i<N; i++) a[i] *= b[i];


int main() 
  dispatch();
  const double mem = 3*sizeof(double)*N*R/1024/1024/1024;
  const double maxbw = 34.1;

  double *a = (double*)_mm_malloc(sizeof *a * N, 32);
  double *b = (double*)_mm_malloc(sizeof *b * N, 32);

  //b must be initialized to get the correct bandwidth!!!
  memset(a, 1, sizeof *a * N);
  memset(b, 1, sizeof *b * N);

  double dtime;
  //dtime = get_wtime(); // call once to fix GCC
  //printf("%f\n", dtime);
  //fix = fix_sse;

  dtime = -get_wtime();
  for(int i=0; i<R; i++) mul(a,b);
  dtime += get_wtime();
  printf("time %.2f s, %.1f GB/s, efficency %.1f%%\n", dtime, mem/dtime, 100*mem/dtime/maxbw);

  _mm_free(a), _mm_free(b);


如果我使用-z now(例如clang -O2 -fno-vectorize -z now foo.c)禁用惰性函数调用解析,那么在第一次调用clock_gettime 之后,Clang 只需要__asm__ __volatile__ ( "vzeroupper" : : : );,就像 GCC 一样。

我预计使用-z now,我只需要在main() 之后立即使用__asm__ __volatile__ ( "vzeroupper" : : : );,但在第一次调用clock_gettime 之后我仍然需要它。

【讨论】:

不错的代码!从this gcc webpage 开始,我不清楚您是否必须在致电__builtin_cpu_supports("avx") 之前致电__builtin_cpu_init (void)。您是否在旧的非 AVX cpu 上测试了您的代码? @wim, dispatch 不应该被评论。那是因为我正在测试 GCC,只需要调用 vzeroupperonce 而不是每次调用。我不知道__builtin_cpu_init。没有它它就可以工作(尽管我没有没有 AVX 的系统来测试)。为了安全起见,我将其添加到我的答案中。 _dl_runtime_resolve_avx 仅在第一次调用时调用来自不同共享库文件的某些函数。尝试禁用延迟绑定(man7.org/linux/man-pages/man1/ld.1.html - “lazy .. 告诉动态链接器将函数调用解析推迟到调用函数时(延迟绑定),而不是在加载时。延迟绑定是默认设置。”) export LD_BIND_NOW=1(man7.org/linux/man-pages/man8/ld.so.8.html - “在程序启动时解析所有符号而不是延迟”)在运行时禁用 _dl_runtime_resolve_avx 的调用。 @osgx -z nowexport LD_BIND_NOW=1 只对 Clang 有影响。由于 Clang 的某些原因,每次调用 clock_gettime(CLOCK_MONOTONIC, &amp;time); 后我都需要 __asm__ __volatile__ ( "vzeroupper" : : : );,除非我使用 -z nowexport LD_BIND_NOW=1。使用 Clang,即使没有 -z nowexport LD_BIND_NOW=1,我也只需要在第一次通话后使用它。 @wim Clang 3.9 不支持__builtin_cpu_init。但它确实支持我的代码中的其他内置函数。

以上是关于AVX 标量运算要快得多的主要内容,如果未能解决你的问题,请参考以下文章

为啥加入从视图中选择前 N 比加入视图要快得多?

SSE向量化与内存对齐的关系

为啥使用声明的变量作为参数调用 UDF 比使用硬编码参数调用要快得多

react-native 渲染速度比从 firebase 加载数据要快得多

基于标量整数条件的AVX向量寄存器的条件移动(cmov)?

MySQL Left Join 运行速度非常慢,拆分为 2 个查询要快得多