随着新一代英特尔处理器的出现,新的且越来越复杂的矢量指令不断涌现。 尽管向量的长度(512位)在不久的将来不会增长,但是会出现新的数据类型和指令类型。 例如,谁能一目了然地了解这种内在函数(以及相应的处理器指令)的作用?
提供执行任何三操作数二进制功能的功能的按位三元逻辑; 特定的二进制函数由imm8中的值指定。
__m512i _mm512_mask_ternarylogic_epi32 (__m512i src, __mmask8 k, __m512i a, __m512i b, int imm8) FOR j := 0 to 15 i := j*32 IF k[j] FOR h := 0 to 31 index[2:0] := (src[i+h] << 2) OR (a[i+h] << 1) OR b[i+h] dst[i+h] := imm8[index[2:0]] ENDFOR ELSE dst[i+31:i] := src[i+31:i] FI ENDFOR dst[MAX:512] := 0
好的,假设我们知道了它是如何工作的。 复杂性的下一个层次是调试大量使用此类内在函数的代码。
那些经常使用内部函数的人都知道这样一个非常有用的站点
-Intel内部函数指南 。 如果仔细查看它的工作原理,很容易注意到javascript前端会下载data-3.xxxml文件,该文件使用类似于Matlab的代码详细描述了所有内在函数。 (例如,我在帖子标题中复制的那个。)
但是,当我们使用内在函数加速代码时,我们不是用Matlab编写,而是用C和C ++编写! 三个月前,一个客户问我C中是否有矢量内在函数的实现用于调试,我决定编写一个解析器,将Intrinsics Guide中的代码转换为C。事实证明,该库实现了几乎所有内在函数,以便您可以使用分步调试器进入内部(或添加调试printf)。
例如,帖子标题的操作变为
for (int j = 0; j <= 15; j++) { if (k & (1 << j)) { for (int h = 0; h <= 31; h++) { int index = ((((src_vec[j] & (1 << h)) >> h) << 2) | (((a_vec[j] & (1 << h)) >> h) << 1) | ((b_vec[j] & (1 << h)) >> h)) & 0x7; dst_vec[j] = (dst_vec[j] & ~(1 << h)) | ((((imm8 & (1 << index)) >> index)) << h); } } else { dst_vec[j] = src_vec[j]; } }
是的,这更容易理解吗? 不是吗 好吧,我只是选择一个复杂的函数作为示例。 通常,当使用内部函数(例如DSP)调试代码时,必须牢记每条指令的算法和功能。 鉴于指令适用于长向量,并且DSP算法通常基于严肃的数学,我的头无法应付-短期记忆和注意力不足。 我怀疑我并不孤单-有几次我什至以为自己在说明中发现了一个错误。 然后,当然,每次发现我错了,打开新的FDIV错误都没有用。 但是,在那种情况下,如果可以在指令中进行逐步调试,那么我将立即理解在什么条件下我的向量的分量中出现了一个我没有想到的值。
客户告诉我,他们使用该库在仅支持AVX2的笔记本电脑上使用AVX-512内部函数调试各个功能。 当然,
英特尔SDE更适合于此-因为它极其精确地模仿了所有指令集。 我有一组单元测试(也是自动生成的),它们针对库中的每个内在函数将其工作结果与相应汇编程序指令的执行结果进行比较。 作为适合单元测试的工具,大多数都可以按预期进行。 但是某些带有浮点数(双精度和单精度)的调试内在函数不一定总是能正确地工作100%。 我会说有时是
-ffast-math 。 并且有不同的舍入机制! IEE754有很多细微之处...
使用immintrin调试而不是SDE还有另一个重要功能(我不以任何方式赞成,但我不能阻止它)。 如果使用选项
-garch = nehalem编译gcc或clang,则gcc和clang从函数堆栈中的函数返回512位向量,而ICC仍将它们返回给ZMM0。 因此,英特尔编译器不能在此模式下使用。 gcc有一个有用的选项
-Og ,它有助于调试,包括immintrin调试。
有几种内部函数,其主要作用是更改寄存器的内容,例如或标志。 我没有执行这些指示。 好吧,虽然我的解析器还没有准备好,但是尚无法实现约10%的内在函数。
使用immintrin调试非常简单-无需更改源代码,但是在进行调试时,必须添加条件编译以包含immintrin_dbg.h而不是immintrin.h。
您可以
在github上下载它。