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 now
或 export LD_BIND_NOW=1
只对 Clang 有影响。由于 Clang 的某些原因,每次调用 clock_gettime(CLOCK_MONOTONIC, &time);
后我都需要 __asm__ __volatile__ ( "vzeroupper" : : : );
,除非我使用 -z now
或 export LD_BIND_NOW=1
。使用 Clang,即使没有 -z now
或 export LD_BIND_NOW=1
,我也只需要在第一次通话后使用它。
@wim Clang 3.9 不支持__builtin_cpu_init
。但它确实支持我的代码中的其他内置函数。以上是关于AVX 标量运算要快得多的主要内容,如果未能解决你的问题,请参考以下文章
为啥使用声明的变量作为参数调用 UDF 比使用硬编码参数调用要快得多