Bee*_*ope 5 c assembly gcc clang inline-assembly
考虑以下小功能:
void foo(int* iptr) {
iptr[10] = 1;
__asm__ volatile ("nop"::"r"(iptr):);
iptr[10] = 2;
}
Run Code Online (Sandbox Code Playgroud)
使用gcc,它将编译为:
foo:
nop
mov DWORD PTR [rdi+40], 2
ret
Run Code Online (Sandbox Code Playgroud)
请特别注意,即在第一次写iptr,iptr[10] = 1根本不会发生:内联汇编nop是在函数的第一件事,只有最后写2(会出现ASM呼叫后)。显然,编译器决定只需要提供其iptr 自身值的最新版本,而不需要提供其指向的内存。
我可以告诉编译器,内存必须是最新的memory,就像这样:
void foo(int* iptr) {
iptr[10] = 1;
__asm__ volatile ("nop"::"r"(iptr):"memory");
iptr[10] = 2;
}
Run Code Online (Sandbox Code Playgroud)
结果为预期的代码:
foo:
mov DWORD PTR [rdi+40], 1
nop
mov DWORD PTR [rdi+40], 2
ret
Run Code Online (Sandbox Code Playgroud)
但是,这太强了,因为它告诉编译器必须写入所有内存。例如,在以下功能中:
void foo2(int* iptr, long* lptr) {
iptr[10] = 1;
lptr[20] = 100;
__asm__ volatile ("nop"::"r"(iptr):);
iptr[10] = 2;
lptr[20] = 200;
}
Run Code Online (Sandbox Code Playgroud)
理想的行为是让编译器优化对第一次写入的优化lptr[20],而不是对第一次写入的优化iptr[10]。的"memory",因为这意味着这两个写有发生撞无法实现这一点:
foo2:
mov DWORD PTR [rdi+40], 1
mov QWORD PTR [rsi+160], 100 ; lptr[10] written unecessarily
nop
mov DWORD PTR [rdi+40], 2
mov QWORD PTR [rsi+160], 200
ret
Run Code Online (Sandbox Code Playgroud)
有没有办法告诉编译器接受gcc扩展的asm语法,则asm的输入包括指针及其可以指向的任何内容?
没错 要求将指针作为内联asm的输入并不意味着指向的内存也是输入或输出,或两者兼而有之。对于寄存器输入和寄存器输出,对于所有gcc来说,您都知道,asm只是通过掩盖低位来对齐指针,或者向其添加常数。(在这种情况下,您希望它可以优化死区存储。)
简单的选择是asm volatile和"memory"破坏者1。
您要求的更窄更具体的方法是使用“虚拟”内存操作数以及寄存器中的指针。您的asm模板不会引用此操作数(除非可能在asm注释中以查看编译器选择了什么)。它告诉编译器您实际读,写或读+写的内存。
虚拟内存输入:
或输出:。或当然具有相同的语法。"m" (*(const int (*)[]) iptr)"=m" (*(int (*)[]) iptr)"+m"
该语法将强制转换为指向数组的指针并取消引用,因此实际输入为C array。(如果您实际上有一个数组,而不是指针,则不需要任何强制转换,只需将其用作内存操作数即可。)
如果您未使用指定大小[],则告诉GCC相对于该指针访问的任何内存都是输入,输出或输入/输出操作数。 如果使用[10]或[some_variable],它将告诉编译器特定的大小。随着运行时间变量的大小,GCC在实践中遗漏了优化iptr[size+1]是不是输入的一部分。
GCC对此进行了记录,因此予以支持。我认为,如果数组元素类型与指针相同,或者如果是,则不是严格混叠违规char。
(来自GCC手册)
一个x86示例,其中字符串内存参数的长度未知。Run Code Online (Sandbox Code Playgroud)asm("repne scasb" : "=c" (count), "+D" (p) : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));
如果可以避免在指针输入操作数上使用早期指针,则虚拟内存输入操作数通常将使用同一寄存器来选择简单的寻址模式。
但是,如果您确实使用Early-Clobber来严格确保asm循环的正确性,则有时虚操作数将使gcc在内存操作数的基址上浪费指令(和额外的寄存器)。检查编译器的asm 输出。
这是inline-asm示例中的一个普遍存在的错误,通常不会被发现,因为asm封装在一个函数中,该函数不会内联到任何调用程序中,这些调用程序会诱使编译器重新排序存储以合并以进行死存储消除。
GNU C内联汇编语法的设计是围绕描述单一指令编译器。目的是告诉编译器有关带有"m"或"=m"操作数约束的内存输入或内存输出,并选择寻址模式。
用内联asm编写整个循环需要格外小心,以确保编译器确实知道发生了什么(或者asm volatile加上了"memory"麻烦),否则您可能会在更改周围的代码或启用允许跨文件内联的链接时优化时面临崩溃的风险。
另请参见使用内联汇编对数组进行循环以将asm语句用作循环主体,仍然在C中执行循环逻辑。使用实际(非虚拟)"m"和"=m"操作数,编译器可以通过在其选择的寻址模式中使用位移来展开循环。 。
脚注1:"memory"破坏器使编译器将asm视为非内联函数调用(可以读取或写入任何内存,但经过转义分析证明尚未转义的本地变量除外)。转义分析包括asm语句本身的输入操作数,还包括任何先前调用可能已将指针存储到的全局或静态变量。因此,通常不必在asm使用"memory"Clobber 的语句周围溢出/重新加载本地循环计数器。
asm volatile 必须确保即使没有使用输出操作数,asm也不会被优化(因为您需要进行未声明的写入内存的副作用)。
或对于仅由asm读取的内存,如果同一输入缓冲区包含不同的输入数据,则需要再次运行asm。没有volatile,asm语句可能会被CSEd循环出局。(一"memory"撞就没有考虑是否当做出优化处理所有的内存作为输入asm的语句甚至需要运行。)
asm没有输出操作数是隐式的volatile,但是将其显式是一个好主意。(GCC手册中有关于asm volatile的部分)。
例如asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory")具有输出操作数,因此不是隐式易失的。如果你像那样使用
arr[5] = 1;
total += asm_sum(arr, len);
memcpy(arr, foo, len);
total += asm_sum(arr, len);
Run Code Online (Sandbox Code Playgroud)
如果没有volatile第二个参数,则asm_sum可以进行优化,假设具有相同输入操作数(指针和长度)的相同asm将产生相同的输出。您需要volatile不是其显式输入操作数的纯函数的任何asm。如果它不优化掉,然后将"memory"撞将具有需要存储器是同步的期望的效果。
| 归档时间: |
|
| 查看次数: |
244 次 |
| 最近记录: |