Bee*_*ope 27 c++ x86 simd icc language-lawyer
考虑以下简单程序:
#include <cstring>
#include <cstdio>
#include <cstdlib>
void replace(char *str, size_t len) {
for (size_t i = 0; i < len; i++) {
if (str[i] == '/') {
str[i] = '_';
}
}
}
const char *global_str = "the quick brown fox jumps over the lazy dog";
int main(int argc, char **argv) {
const char *str = argc > 1 ? argv[1] : global_str;
replace(const_cast<char *>(str), std::strlen(str));
puts(str);
return EXIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)
它在命令行上使用(可选)字符串并打印它,并/
替换为字符_
.该替换功能由c_repl
功能1实现.例如,a.out foo/bar
打印:
foo_bar
Run Code Online (Sandbox Code Playgroud)
到目前为止的基本内容,对吧?
如果你没有指定一个字符串,它可以方便地使用全局字符串,快速棕色狐狸跳过懒狗,它不包含任何/
字符,因此不会进行任何替换.
当然,字符串常量是const char[]
,所以我需要先抛弃常量- 这就是const_cast
你所看到的.由于字符串从未被实际修改过,因此我认为这是合法的.
gcc和clang编译具有预期行为的二进制文件,无论是否在命令行上传递字符串.但是,当您不提供字符串时,icc会崩溃:
icc -xcore-avx2 char_replace.cpp && ./a.out
Segmentation fault (core dumped)
Run Code Online (Sandbox Code Playgroud)
根本原因是主循环,c_repl
如下所示:
400c0c: vmovdqu ymm2,YMMWORD PTR [rsi]
400c10: add rbx,0x20
400c14: vpcmpeqb ymm3,ymm0,ymm2
400c18: vpblendvb ymm4,ymm2,ymm1,ymm3
400c1e: vmovdqu YMMWORD PTR [rsi],ymm4
400c22: add rsi,0x20
400c26: cmp rbx,rcx
400c29: jb 400c0c <main+0xfc>
Run Code Online (Sandbox Code Playgroud)
它是一个矢量化循环.基本思想是加载32个字节,然后与/
字符进行比较,形成一个掩码值,每个匹配的字节设置一个字节,然后将现有字符串与包含32个_
字符的向量混合,有效地只替换/
字符.最后,使用vmovdqu YMMWORD PTR [rsi],ymm4
指令将更新的寄存器写回字符串.
此最终存储崩溃,因为该字符串是只读的并在.rodata
二进制文件中分配,该文件使用只读页面加载.当然,商店是一个逻辑"无操作",回写它读取的相同字符,但CPU并不关心!
我的代码是合法的C++,因此我应该责怪icc错误编译这个,或者我在某处趟过UB沼泽?
1同一个问题的崩溃发生std::replace
在一个std::string
而不是我的"C-like"代码上,但我想尽可能地简化分析并使其完全自包含.
Pet*_*des 22
据我所知,你的程序格式正确,没有未定义的行为.C++抽象机器实际上从未分配给const
对象. 未采取if()
足以"隐藏"/"保护"如果执行UB的事情. 唯一if(false)
不能救你的是一个格式错误的程序,例如语法错误或尝试使用此编译器或目标arch上不存在的扩展.
通常不允许编译器发明if-conversion转换为无分支代码的写入.
抛弃const
是合法的,只要你不通过它实际分配.例如,用于将指针传递给不是const-correct的函数,并且使用非const
指针的只读输入.您链接的答案是否允许在const定义的对象上抛弃const,只要它实际上没有被修改?是正确的.
ICC在这里的行为不是 ISO C++或C中UB的证据. 我认为你的推理是合理的,这是明确定义的.你发现了一个ICC错误.如果有人关心,请在他们的论坛上报告:https://software.intel.com/en-us/forums/intel-c-compiler.开发人员已接受其论坛该部分中的现有错误报告,例如此报告.
我们可以构造一个示例,它以相同的方式自动向量化(使用无条件和非原子读取/可能 - 修改/重写),这显然是非法的,因为读取/重写发生在C抽象机器的第二个字符串上甚至不读.
因此,我们不能相信ICC的代码来告诉我们什么时候我们已经导致UB,因为即使在明显合法的情况下它也会使代码崩溃.
Godbolt:ICC19.0.1 -O2 -march=skylake
(较旧的ICC仅理解类似的选项-xcore-avx2
,但现代ICC理解与-march
GCC/clang 相同.)
#include <stddef.h>
void replace(const char *str1, char *str2, size_t len) {
for (size_t i = 0; i < len; i++) {
if (str1[i] == '/') {
str2[i] = '_';
}
}
}
Run Code Online (Sandbox Code Playgroud)
它检查之间的重叠str1[0..len-1]
和str2[0..len-1]
,但足够大,len
并且没有重叠,将使用这个内部循环:
..B1.15: # Preds ..B1.15 ..B1.14 //do{
vmovdqu ymm2, YMMWORD PTR [rsi+r8] #6.13 // load from str2
vpcmpeqb ymm3, ymm0, YMMWORD PTR [rdi+r8] #5.24 // compare vs. str1
vpblendvb ymm4, ymm2, ymm1, ymm3 #6.13 // blend
vmovdqu YMMWORD PTR [r8+rsi], ymm4 #6.13 // store to str2
add r8, 32 #4.5 // i+=32
cmp r8, rax #4.5
jb ..B1.15 # Prob 82% #4.5 // }while(i<len);
Run Code Online (Sandbox Code Playgroud)
对于线程安全,众所周知,通过非原子读/重写发明写是不安全的.
C++抽象机器根本不会触及str2
,因此使单字符串版本的任何参数无效,因为数据竞争UB是不可能的,因为同时读取str
另一个线程正在写入它已经是UB.即使是C++ 20std::atomic_ref
也不会改变它,因为我们正在阅读非原子指针.
但更糟糕的是,str2
可以nullptr
. 或指向靠近对象的末尾(恰好存储在页面末尾附近),str1
包含字符,以便不会发生超过str2
页面末尾的写入.我们甚至可以安排最后一个byte(str2[len-1]
)在一个新页面中,这样它就是一个有效对象的结尾.构造这样的指针甚至是合法的(只要你没有deref).但通过合法是合法的str2=nullptr
; if()
不运行的代码背后的代码不会导致UB.
或者另一个线程并行运行相同的搜索/替换功能,使用不同的键/替换来写入不同的元素str2
. 未修改值的非原子加载/存储将踩到来自另一个线程的修改值.根据C++ 11内存模型,它绝对允许不同的线程同时触摸同一阵列的不同元素. char数组上的C++内存模型和竞争条件.(这就是为什么char
必须与目标机器在没有非原子RMW的情况下可以写入的最小内存单元一样大.但是,存储到缓存中的字节的内部原子RMW很好,并且不会停止字节存储指令从有用.)
(这个例子只适用于单独的str1/str2版本,因为读取每个元素意味着线程将读取数组元素,而另一个线程可能在写入的中间,即数据竞争UB.)
正如Herb Sutter在atomic<>
武器中提到的:C++内存模型和现代硬件第2部分:编译器和硬件的限制(包括常见错误) ; x86/x64,IA64,POWER,ARM等上的代码生成和性能; 放松的原子; volatile:在C++ 11标准化之后,淘汰非原子RMW代码一直是编译器的一个持续问题.我们大部分都在那里,但像ICC这样的高度积极和不那么主流的编译器显然仍然存在缺陷.
(但是,我非常有信心英特尔编译器开发人员会认为这是一个错误.)
一些不太合理(在真实程序中看到)的例子,这也会破坏:
此外nullptr
,您可以传递指向(数组)std::atomic<T>
或互斥体的指针,其中非原子读取/重写通过发明写入来破坏事物.(char*
可以别名任何东西).
或者str2
指向你为动态分配而划分的缓冲区,并且早期部分str1
将有一些匹配,但后面的部分str1
将没有任何匹配,并且其他部分str2
正在被其他线程使用.(由于某种原因,你不能轻易计算出阻止循环的长度).
对于未来的读者:如果你想让编译器以这种方式自动矢量化:
您可以编写源代码str2[i] = x ? replacement : str2[i];
,始终将字符串写入C++抽象机器中.IIRC,让gcc/clang按照ICC在进行不安全的if-conversion混合后的方式进行矢量化.
从理论上讲,优化编译器可以将其转换回标量清理中的条件分支,或者不必要地避免弄脏内存.(或者,如果针对可能存在谓词存储的ARM32之类的ISA,而不是仅仅ALU选择操作,如x86 cmov
,PowerPC isel
或AArch64 csel
.如果谓词为假,则ARM32谓词指令在体系结构上是NOP).
或者,如果x86编译器选择使用AVX512屏蔽存储,那么也可以安全地按照ICC的方式进行向量化:屏蔽存储执行故障抑制,并且从不实际存储到掩码为假的元素.(当使用带AVX-512加载和存储的掩码寄存器时,是否会因屏蔽元素的无效访问而引发错误?).
vpcmpeqb k1, zmm0, [rdi] ; compare from memory into mask
vmovdqu8 [rsi]{k1}, zmm1 ; masked store that only writes elements where the mask is true
Run Code Online (Sandbox Code Playgroud)
ICC19实际上基本上做了这个(但使用索引寻址模式)-march=skylake-avx512
.但是使用ymm向量,因为512位降低了最大turbo太多值得它,除非你的整个程序大量使用AVX512,无论如何在Skylake Xeons上.
所以我认为ICC19在使用AVX512进行矢量化时是安全的,而不是AVX2.除非在清理代码中出现问题,否则它会执行更复杂的vpcmpuq
和kshift
/ kor
,零掩码加载,以及屏蔽比较到另一个掩码reg.
AVX1已经屏蔽了vmaskmovps/pd
具有故障抑制功能的存储()以及所有功能,但在AVX512BW之前,没有比32位更窄的粒度.AVX2整数版本仅在dword/qword粒度中可用vpmaskmovd/q
.
归档时间: |
|
查看次数: |
511 次 |
最近记录: |