Visual Studio的CPU分派程序:针对AVX和SSE。

8
我使用两台电脑,一台支持 AVX,另一台不支持。希望我的代码能够在运行时查找 CPU 支持的指令集,并选择合适的代码路径,这样会很方便。
我已经按照 Agner Fog 的建议编写了一个 CPU dispatcher (http://www.agner.org/optimize/#vectorclass)。但是,在不支持 AVX 的机器上,使用 Visual Studio 编译和链接启用了 AVX 的代码时,运行后会导致代码崩溃。
举个例子,我有两个源文件,一个定义了 SSE2 指令集并包含一些 SSE2 指令,另一个定义了 AVX 指令集并包含一些 AVX 指令。在我的主函数中,如果我只引用 SSE2 函数,则代码仍然会崩溃,因为其中任何源代码启用了 AVX 并包含 AVX 指令。您有什么解决方法吗?
编辑: 好的,我想我找到了问题所在。我正在使用 Agner Fog 的向量类,并将其定义为三个源文件:
//file sse2.cpp - compiled with /arch:SSE2
#include "vectorclass.h"
float func_sse2(const float* a) {
    Vec8f v1 = Vec8f().load(a);
    float sum = horizontal_add(v1);
    return sum;
}
//file avx.cpp - compiled with /arch:AVX
#include "vectorclass.h"
float func_avx(const float* a) {
    Vec8f v1 = Vec8f().load(a);
    float sum = horizontal_add(v1);
    return sum;
}
//file foo.cpp - compiled with /arch:SSE2
#include <stdio.h>
extern float func_sse2(const float* a);
extern float func_avx(const float* a);
int main() {
    float (*fp)(const float*a); 
    float a[] = {1,2,3,4,5,6,7,8};
    int iset = 6;
    if(iset>=7) { 
        fp = func_avx;  
    }
    else { 
        fp = func_sse2;
    }
    float sum = (*fp)(a);
    printf("sum %f\n", sum);
}

这段代码会崩溃。但如果我在func_SSE2中使用Vec4f,则不会崩溃。 我不理解这个问题。只要没有另一个AVX源文件,我就可以单独使用Vec8f和SSE2。Agner Fog的手册说:

“除非指定了AVX指令集,否则使用256位浮点向量类(Vec8f、Vec4d)没有优势,但是如果同一源代码在有或没有AVX的情况下使用,则使用这些类可能很方便。在编译时,每个256位向量将被拆分为两个128位向量。”

但是,当我有两个源文件都含有Vec8f,其中一个使用SSE2编译,另一个使用AVX编译时,程序会崩溃。

编辑2:我可以从命令行中让它工作。

>cl -c sse2.cpp
>cl -c /arch:AVX avx.cpp
>cl foo.cpp sse2.obj avx.obj
>foo.exe

编辑3: 然而,这会导致崩溃。

>cl -c sse2.cpp
>cl -c /arch:AVX avx.cpp
>cl foo.cpp avx.obj sse2.obj
>foo.exe

另一个线索是,链接的顺序很重要。如果avx.obj在sse2.obj之前,则会崩溃,但如果sse2.obj在avx.obj之前,则不会崩溃。我不确定它是否选择了正确的代码路径(我现在无法访问我的AVX系统),但至少它不会崩溃。


崩溃的详细信息是什么?你是否已经在调试器中确定了失败的指令? - Stephen Canon
调试器显示func_SSE正在尝试使用AVX指令。我不知道为什么。但是我成功地通过命令行使代码在不崩溃的情况下工作了。我添加了上面的命令。我仍然不知道如何在IDE中实现它。另一方面,我第一次在Windows上从命令行编译!这是我在Linux上编译的唯一方式。 - user2088790
不,我没有使用链接时代码生成。但我尝试过了,它并没有产生任何影响。 - user2088790
我找到了另一个线索。如果avx.obj在sse2.obj之前,它会崩溃,但否则它可以正常工作。 - user2088790
结论是什么,Agner的库是否进行了运行时分派? - Royi
显示剩余2条评论
3个回答

9
我知道这是一个旧问题,问题提出者似乎已经不在了,但昨天我也遇到了同样的问题。以下是我的解决方法。
当编译 sse2.cpp 和 avx.cpp 文件时,它们生成的目标文件不仅包含你的函数,还包含任何所需的模板函数。例如 Vec8f::load。这些模板函数也使用所请求的指令集编译。
这意味着,你的 sse2.obj 和 avx.obj 目标文件都将包含使用相应指令集编译的 Vec8f::load 的定义。
但由于编译器将 Vec8f::load 视为外部可见,因此它将其放置在对象文件的“COMDAT”节中,并带有“selectany”(又称“pick any”)标签。这告诉链接器,如果它看到该符号的多个定义(例如在两个不同的目标文件中),则可以选择任何一个。这样做是为了减少最终可执行文件中重复代码的大小,否则模板和内联函数的多个定义会膨胀文件大小。
你遇到的问题直接与此有关,因为传递给链接器的目标文件的顺序会影响它选择的文件。具体来说,在这里,它似乎选择它看到的第一个定义。
如果选择的是 avx.obj,则始终使用 AVX 编译版本的 Vec8F::load。这将在不支持该指令集的机器上崩溃。另一方面,如果 sse2.obj 是第一个,则始终使用 SSE2 编译版本。即使支持 AVX,也只会使用 SSE2 指令。
如果查看链接器“map”文件输出(使用 /map 选项生成),就可以看到这种情况。以下是相关的(编辑过的)摘录。
//
// link with sse2.obj before avx.obj
//
0001:00000080  _main                             foo.obj
0001:00000330  func_sse2@@YAMPBM@Z               sse2.obj
0001:00000420  ??0Vec256fe@@QAE@XZ               sse2.obj
0001:00000440  ??0Vec4f@@QAE@ABT__m128@@@Z       sse2.obj
0001:00000470  ??0Vec8f@@QAE@XZ                  sse2.obj <-- sse2 version used
0001:00000490  ??BVec4f@@QBE?AT__m128@@XZ        sse2.obj
0001:000004c0  ?get_high@Vec8f@@QBE?AVVec4f@@XZ  sse2.obj
0001:000004f0  ?get_low@Vec8f@@QBE?AVVec4f@@XZ   sse2.obj
0001:00000520  ?load@Vec8f@@QAEAAV1@PBM@Z        sse2.obj <-- sse2 version used
0001:00000680  ?func_avx@@YAMPBM@Z               avx.obj
0001:00000740  ??BVec8f@@QBE?AT__m256@@XZ        avx.obj

//
// link with avx.obj before sse2.obj
//
0001:00000080  _main                             foo.obj
0001:00000270  ?func_avx@@YAMPBM@Z               avx.obj
0001:00000330  ??0Vec8f@@QAE@XZ                  avx.obj <-- avx version used
0001:00000350  ??BVec8f@@QBE?AT__m256@@XZ        avx.obj
0001:00000380  ?load@Vec8f@@QAEAAV1@PBM@Z        avx.obj <-- avx version used
0001:00000580  ?func_sse2@@YAMPBM@Z              sse2.obj
0001:00000670  ??0Vec256fe@@QAE@XZ               sse2.obj
0001:00000690  ??0Vec4f@@QAE@ABT__m128@@@Z       sse2.obj
0001:000006c0  ??BVec4f@@QBE?AT__m128@@XZ        sse2.obj
0001:000006f0  ?get_high@Vec8f@@QBE?AVVec4f@@XZ  sse2.obj
0001:00000720  ?get_low@Vec8f@@QBE?AVVec4f@@XZ   sse2.obj

至于修复它,那是另一回事。在这种情况下,以下的粗暴解决方案应该可以通过强制AVX版本拥有其自己不同命名的模板函数来实现。虽然SSE2和AVX版本相同,但这将增加生成的可执行文件的大小,因为它将包含多个相同函数的版本。

// avx.cpp
namespace AVXWrapper {
\#include "vectorclass.h"
}
using namespace AVXWrapper;

float func_avx(const float* a)
{
    ...
}

但是有一些重要的限制 - (a) 如果包含的文件管理任何形式的全局状态,它将不再是真正的全局状态,因为您将会有 2 个 '半全局' 版本, (b) 您将无法在其他代码和在 avx.cpp 中定义的函数之间传递 vectorclass 变量作为参数。


如果函数是相同的,链接器就能够将它们“折叠”在一起并删除多余的一个。 - Bas

2
链接顺序的重要性让我想到 obj 文件中可能存在某种初始化代码。如果初始化代码是公共的,则只会采用第一个。 我无法重现它,但您应该能够在汇编清单中看到它(使用 /c /Ftestavx.asm 进行编译)。

调度程序在我的AVX系统上不会崩溃,但在没有AVX的系统上会崩溃。你能否在没有AVX的系统上进行测试?也许具有AVX的系统也没有选择SSE指令,但由于它具有AVX,因此仍然可以工作?汇编清单对我来说有点太高级了,所以我可能需要回到这个问题。 - user2088790

1
将SSE和AVX函数放在不同的CPP文件中,并确保编译没有使用/arch:AVX的SSE版本。

这正是我所做的。 - user2088790
只需在调试器下运行它。当CPU生成“无效指令”异常时,您将看到此指令的来源。您使用的某些SSE指令可能不受您的非AVX CPU支持。有许多代SSE指令:SSE,SSE2,SSE3,SSSE3,SSE4.1,SSE4.2和SSE4A(包括SSE3,但不包括SSSE3,SSE4.1或SSE4.2)。 - Marat Dukhan
我的CPU支持SSE4.2。我用CPU-Z进行了检查。但现在我正在尝试一个没有向量类的简化版本的代码,它正在工作。我会回来告诉你的... - user2088790
我在我的问题中添加了一些文本,可能有助于解释事情。 - user2088790

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接