C++集中SIMD使用

ace*_*red 5 c++ optimization simd

我有一个图书馆和很多项目,取决于该库.我想使用SIMD扩展优化库内的某些过程.然而,对我来说保持便携是很重要的,所以对用户来说它应该是非常抽象的.我在开始时说,我不想使用其他一些很棒的库.我实际上想要了解我想要的是可能的以及在多大程度上.

我的第一个想法是拥有一个"向量"包装类,SIMD的使用对用户是透明的,如果目标机器上没有SIMD扩展,则可以使用"标量"向量类.我想到了天真的想法,使用预处理器从多个中选择一个矢量类,具体取决于编译库的目标.所以一个标量向量类,一个带SSE(基本上是这样的:http://fastcpp.blogspot.de/2011/12/simple-vector3-class-with-sse-support.html)等...所有具有相同的界面.这给了我很好的性能,但这意味着我必须为我使用的任何类型的SIMD ISA编译库.我宁愿在运行时动态评估处理器功能并选择"最佳"

所以我的第二个猜测是使用抽象方法获得一般的"向量"类."处理器评估程序"功能将返回最佳实现的实例.显然这会导致丑陋的代码,但是指向vector对象的指针可以存储在类似智能指针的容器中,该容器只是将调用委托给vector对象.实际上我更喜欢这种方法,因为它的抽象,但我不确定调用虚方法实际上是否会破坏我使用SIMD扩展获得的性能.

我想出的最后一个选项是优化整个例程,并在运行时选择最佳例程.我不喜欢这个想法,因为这迫使我多次实现整个功能.我宁愿这样做一次,使用我对vector类的想法,我想做这样的事情,例如:

void Memcopy(void *dst, void *src, size_t size)
{
    vector v;
    for(int i = 0; i < size; i += v.size())
    {
        v.load(src);
        v.store(dst);
        dst += v.size();
        src += v.size();
    }
}
Run Code Online (Sandbox Code Playgroud)

我在这里假设"大小"是一个正确的值,因此不会发生重叠.这个例子应该只显示我更喜欢的东西.例如,在使用SSE的情况下,向量对象的大小方法将返回4,在使用标量版本的情况下,返回1.有没有一种正确的方法来实现这一点,只使用运行时信息而不会失去太多的性能?抽象对我来说比性能更重要,但由于这是性能优化,如果不加速我的应用程序,我不会包括它.

我也在网上找到了这个:http://compeng.uni-frankfurt.de/?vc 它的开源但我不明白如何选择正确的矢量类.

Pet*_*des 5

如果所有内容都在编译时内联,那么您的想法将只编译为高效代码,这与运行时CPU调度不兼容.对于v.load(),v.store()和v.size()在运行时实际上根据CPU而不同,它们必须是实际的函数调用,而不是单个指令.开销将是杀手.


如果您的库具有足够大的功能而无需内联工作,那么函数指针非常适合基于运行时CPU检测的调度.(例如,制作memcpy的多个版本,并为每次调用支付一次运行时检测的开销,而不是每次循环迭代两次.)

应该在您的库的外部API/ABI中不可见,除非您的函数非常短,以至于额外(直接)调用/ ret的开销很重要.在库函数的实现中,将要创建CPU特定版本的每个子任务放入辅助函数中.通过函数指针调用这些辅助函数.


从函数指针开始,将其初始化为适用于基线目标的版本.例如,用于x86-64的SSE2,用于传统32位x86的标量或SSE2(取决于您是否关心Athlon XP和Pentium III),以及可能是非x86架构的标量.在构造函数或库init函数中,执行CPUID并将函数指针更新为主机CPU的最佳版本.即使您的绝对基线是标量,您也可以使您的"良好性能"基线像SSSE3一样,并且不会花费太多/任何时间在仅SSE2的例程上.即使你主要针对SSSE3,你的一些例程可能最终只需要SSE2,所以你也可以将它们标记为这样,并让调度程序在仅执行SSE2的CPU上使用它们.

更新函数指针甚至不需要任何锁定.在构造函数完成设置函数指针之前从其他线程发生的任何调用都可能获得基线版本,但这很好.在x86上存储指向对齐地址的指针是原子的.如果它在任何需要运行时CPU检测的例程的平台上都不是原子的,那么使用C++ std:atomic(使用内存顺序缓存存储和加载,而不是默认的顺序一致性,这将导致每个内存都有一个完整的内存屏障)加载).通过函数指针调用时,开销很小很重要,并且不同的线程看到函数指针的更改的顺序并不重要.他们只写了一次.


x264(经过大量优化的开源h.264视频编码器)广泛使用这种技术,具有函数指针数组.x264_mc_init_mmx()例如,请参阅.(该功能处理运动补偿功能的所有CPU调度,从MMX到AVX2).我假设libx264在"编码器初始化"功能中执行CPU调度.如果您没有要求库的用户调用的函数,那么当您使用库的程序启动时,您应该研究某种运行全局构造函数/初始化函数的机制.


如果你想让它与非常C++的代码一起使用(C++ ish?这是一个词吗?),即模板化的类和函数,使用该库的程序可能会进行CPU调度,并安排获得基线和编译的多个CPU要求版本的函数.


Z b*_*son 1

我通过分形项目正是这样做的。对于浮点型,它适用于 1、2、4、8 和 16 的向量大小;对于双精度型,它适用于 1、2、4、8 的向量大小。我在运行时使用 CPU 调度程序来选择以下指令集:SSE2、SSE4.1、AVX、AVX+FMA 和 AVX512。

我使用向量大小 1 的原因是为了测试性能。已经有一个 SIMD 库可以完成这一切:Agner Fog 的Vector Class Library。他甚至还提供了 CPU 调度程序的示例代码。

VCL 在仅具有 SSE(甚至用于 SSE 的 AVX512)的系统上模拟 AVX 等硬件。它只实现 AVX 两次(AVX512 实现四次),因此在大多数情况下,您可以只使用您想要定位的最大向量大小。

//#include "vectorclass.h"
void Memcopy(void *dst, void *src, size_t size)
{
    Vec8f v; //eight floats using AVX hardware or AVX emulated with SSE twice.
    for(int i = 0; i < size; i +=v.size())
    {
        v.load(src);
        v.store(dst);
        dst += v.size();
        src += v.size();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,编写高效的 memcpy 很复杂。对于大尺寸,您应该考虑非 temroal 存储,并在 IVB 及以上使用rep movsb)。 请注意,该代码与您要求的代码相同,只是我将单词更改vectorVec8f

使用 VLC 作为 CPU 调度程序、模板和宏,您可以编写代码/内核,使其看起来与标量代码几乎相同,而无需针对每个不同的指令集和向量大小重复源代码。更大的是您的二进制文件,而不是您的源代码。

我已经多次描述过 CPU 调度程序。您还可以在此处查看一些使用调度程序模板和宏的示例:函数模板的别名

编辑:这是我的内核的一部分的示例,用于计算等于矢量大小的一组像素的 Mandelbrot 集。在编译时,我将 TYPE 设置为floatdoubledoubledouble,并将 N 设置为 1、2、4、8 或 16。此处doubledouble描述了我创建并添加到 VCL 中的类型。这会生成 Vec1f、Vec4f、Vec8f、Vec16f、Vec1d、Vec2d、Vec4d、Vec8d、doubledouble1、doubledouble2、doubledouble4、doubledouble8 的向量类型。

template<typename TYPE, unsigned N>
static inline intn calc(floatn const &cx, floatn const &cy, floatn const &cut, int32_t maxiter) {
    floatn x = cx, y = cy;
    intn n = 0; 
    for(int32_t i=0; i<maxiter; i++) {
        floatn x2 = square(x), y2 = square(y);
        floatn r2 = x2 + y2;
        booln mask = r2<cut;
        if(!horizontal_or(mask)) break;
        add_mask(n,mask);
        floatn t = x*y; mul2(t);
        x = x2 - y2 + cx;
        y = t + cy;
    }
    return n;
}
Run Code Online (Sandbox Code Playgroud)

因此,我针对几种不同数据类型和向量大小的 SIMD 代码几乎与我将使用的标量代码相同。我没有包含在每个超像素上循环的内核部分。

我的构建文件看起来像这样

g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -msse2          -Ivectorclass  kernel.cpp -okernel_sse2.o
g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -msse4.1        -Ivectorclass  kernel.cpp -okernel_sse41.o
g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -mavx           -Ivectorclass  kernel.cpp -okernel_avx.o
g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -mavx2 -mfma    -Ivectorclass  kernel.cpp -okernel_avx2.o
g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -mavx2 -mfma    -Ivectorclass  kernel_fma.cpp -okernel_fma.o
g++ -m64 -c -Wall -g -std=gnu++11 -O3 -fopenmp -mfpmath=sse -mavx512f -mfma -Ivectorclass  kernel.cpp -okernel_avx512.o
g++ -m64 -Wall -Wextra -std=gnu++11 -O3 -fopenmp -mfpmath=sse -msse2 -Ivectorclass frac.cpp vectorclass/instrset_detect.cpp kernel_sse2.o kernel_sse41.o kernel_avx.o kernel_avx2.o kernel_avx512.o kernel_fma.o -o frac
Run Code Online (Sandbox Code Playgroud)

然后调度员看起来像这样

int iset = instrset_detect();
fp_float1  = NULL; 
fp_floatn  = NULL;
fp_double1 = NULL;
fp_doublen = NULL;
fp_doublefloat1  = NULL;
fp_doublefloatn  = NULL;
fp_doubledouble1 = NULL;
fp_doubledoublen = NULL;
fp_float128 = NULL;
fp_floatn_fma = NULL;
fp_doublen_fma = NULL;

if (iset >= 9) {
    fp_float1  = &manddd_AVX512<float,1>;
    fp_floatn  = &manddd_AVX512<float,16>;
    fp_double1 = &manddd_AVX512<double,1>;
    fp_doublen = &manddd_AVX512<double,8>;
    fp_doublefloat1  = &manddd_AVX512<doublefloat,1>;
    fp_doublefloatn  = &manddd_AVX512<doublefloat,16>;
    fp_doubledouble1 = &manddd_AVX512<doubledouble,1>;
    fp_doubledoublen = &manddd_AVX512<doubledouble,8>;
}
else if (iset >= 8) {
    fp_float1  = &manddd_AVX<float,1>;
    fp_floatn  = &manddd_AVX2<float,8>;
    fp_double1 = &manddd_AVX2<double,1>;
    fp_doublen = &manddd_AVX2<double,4>;
    fp_doublefloat1  = &manddd_AVX2<doublefloat,1>;
    fp_doublefloatn  = &manddd_AVX2<doublefloat,8>;
    fp_doubledouble1 = &manddd_AVX2<doubledouble,1>;
    fp_doubledoublen = &manddd_AVX2<doubledouble,4>;
}
....
Run Code Online (Sandbox Code Playgroud)

这将函数指针设置为运行时找到的指令集的每个不同的可能数据类型向量组合。然后我可以调用我感兴趣的任何函数。