ASM是啥?⊙∀⊙?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASM是啥?⊙∀⊙?相关的知识,希望对你有一定的参考价值。
参考技术A 自动存储管理(ASM,Automatic Storage Management)ASM 是 Oracle 数据库 10g 中一个非常出色的新特性,它以平台无关的方式提供了文件系统、逻辑卷管理器以及软件 RAID(磁盘阵列) 等服务。ASM 可以条带化和镜像磁盘,从而实现了在数据库被加载的情况下添加或移除磁盘以及自动平衡 I/O 以删除“热点”。它还支持直接和异步的 I/O 并使用 Oracle9i 中引入的 Oracle 数据管理器 API(简化的 I/O 系统调用接口)。
'asm','__ asm'和'__asm__'有什么区别?
据我所知,__asm { ... };
和__asm__("...");
之间的唯一区别是,第一个使用mov eax, var
,第二个使用movl %0, %%eax
和:"=r" (var)
。还有什么其他差异?那只是asm
呢?
您使用哪一个取决于您的编译器。这不像C语言那样标准。
MSVC内联asm和GNU C内联asm之间存在巨大差异。 GCC语法设计用于最佳输出而不会浪费指令,用于包装单个指令或其他内容。 MSVC语法设计得相当简单,但是AFAICT如果没有延迟和额外的指令通过内存为您的输入和输出使用它是不可能的。
如果出于性能原因使用内联asm,这使得MSVC内联asm仅在完全在asm中编写完整循环时才可行,而不是用于在内联函数中包装短序列。下面的例子(用函数包装idiv
)是MSVC不好的东西:~8个额外的存储/加载指令。
MSVC内联asm(由MSVC使用,可能是icc,也可能在一些商业编译器中提供):
- 查看你的asm,找出你的代码所处的寄存器。
- 只能通过内存传输数据。例如,存储在寄存器中的数据由编译器存储,以准备您的
mov ecx, shift_count
。因此,使用编译器不会为您生成的单个asm指令,包括在路上和路上的往返内存。 - 更适合初学者,但通常无法避免数据输入/输出。即使除了语法限制之外,当前版本的MSVC中的优化器也不擅长围绕内联asm块进行优化。
GNU C inline asm is not a good way to learn asm。您必须非常了解asm,以便您可以告诉编译器您的代码。你必须了解编译器需要知道什么。该答案还与其他inline-asm指南和Q&A有关。 x86标签维基对于asm一般有很多好东西,但只是链接到GNU内联asm。 (该答案中的内容也适用于非x86平台上的GNU内联asm。)
GNU C inline asm语法由gcc,clang,icc和一些实现GNU C的商业编译器使用:
- 你必须告诉编译器你破坏了什么。如果不这样做,将导致以非显而易见的难以调试的方式破坏周围的代码。
- 功能强大但难以阅读,学习和使用语法来告诉编译器如何提供输入以及在何处查找输出。例如
"c" (shift_count)
将让编译器在你的内联asm运行之前将shift_count
变量放入ecx
。 - 因为asm必须在一个字符串常量内,所以对于大块代码来说是额外的笨重。所以你通常需要
"insn %[inputvar], %%reg " // comment "insn2 %%reg, %[outputvar] "
- 非常无情/更难,但允许更低的开销esp。用于包装单个指令。 (包装单个指令是原始的设计意图,这就是为什么你必须特别告诉编译器有关早期的clobbers,以阻止它使用相同的寄存器输入和输出,如果这是一个问题。)
Example: full-width integer division (div
)
在32位CPU上,将64位整数除以32位整数,或者进行全乘(32x32-> 64),可以从内联asm中受益。 gcc和clang没有利用idiv
来获取(int64_t)a / (int32_t)b
,可能是因为如果结果不适合32位寄存器,则指令会出错。因此,与this Q&A about getting quotient and remainder from one div
不同,这是内联asm的用例。 (除非有办法通知编译器结果是否合适,所以idiv不会出错。)
我们将使用调用约定将一些args放在寄存器中(甚至在正确的寄存器中使用hi
),以显示一个更接近你在内联这样一个小函数时看到的情况。
MSVC
使用inline-asm时要小心register-arg调用约定。显然,内联-asm支持是如此糟糕的设计/实现the compiler might not save/restore arg registers around the inline asm, if those args aren't used in the inline asm。感谢@RossRidge指出这一点。
// MSVC. Be careful with _vectorcall & inline-asm: see above
// we could return a struct, but that would complicate things
int _vectorcall div64(int hi, int lo, int divisor, int *premainder) {
int quotient, tmp;
__asm {
mov edx, hi;
mov eax, lo;
idiv divisor
mov quotient, eax
mov tmp, edx;
// mov ecx, premainder // Or this I guess?
// mov [ecx], edx
}
*premainder = tmp;
return quotient; // or omit the return with a value in eax
}
更新:显然在eax
或edx:eax
留下一个值,然后从无效函数(没有return
)is supported, even when inlining的末尾掉下来。我认为这只有在asm
语句之后没有代码时才有效。这避免了输出的存储/重载(至少对于quotient
),但我们无法对输入做任何事情。在具有堆栈args的非内联函数中,它们已经在内存中,但在这个用例中,我们正在编写一个可以有用内联的小函数。
编译与MSVC 19.00.23026 /O2
on rextester(与main()
找到exe和dumps the compiler's asm output to stdout的目录)。
## My added comments use. ##
; ... define some symbolic constants for stack offsets of parameters
; 48 : int ABI div64(int hi, int lo, int divisor, int *premainder) {
sub esp, 16 ; 00000010H
mov DWORD PTR _lo$[esp+16], edx ## these symbolic constants match up with the names of the stack args and locals
mov DWORD PTR _hi$[esp+16], ecx
## start of __asm {
mov edx, DWORD PTR _hi$[esp+16]
mov eax, DWORD PTR _lo$[esp+16]
idiv DWORD PTR _divisor$[esp+12]
mov DWORD PTR _quotient$[esp+16], eax ## store to a local temporary, not *premainder
mov DWORD PTR _tmp$[esp+16], edx
## end of __asm block
mov ecx, DWORD PTR _premainder$[esp+12]
mov eax, DWORD PTR _tmp$[esp+16]
mov DWORD PTR [ecx], eax ## I guess we should have done this inside the inline asm so this would suck slightly less
mov eax, DWORD PTR _quotient$[esp+16] ## but this one is unavoidable
add esp, 16 ; 00000010H
ret 8
有大量额外的mov指令,编译器甚至没有接近优化任何它。我想也许它会看到并理解内联asm中的mov tmp, edx
,并将它作为premainder
的商店。但是,我想这需要在内联asm块之前将堆栈中的premainder
加载到寄存器中。
_vectorcall
实际上比使用正常的堆栈ABI更糟糕。寄存器中有两个输入,它将它们存储到内存中,因此内联asm可以从命名变量加载它们。如果这是内联的,那么更多的参数可能会出现在regs中,而且必须将它们全部存储起来,所以asm会有内存操作数!因此,与gcc不同,我们从内联中获得的收益并不高。
在asm块中执行*premainder = tmp
意味着在asm中编写更多代码,但确实避免了余数的完全braindead存储/加载/存储路径。这将指令数量减少了2个,减少到11个(不包括ret
)。
我试图从MSVC中获取最好的代码,而不是“使用它错误”并创建一个稻草人的论点。但AFAICT包装非常短的序列非常可怕。据推测,有64/32 - > 32除法的内在函数允许编译器为这种特殊情况生成良好的代码,因此在MSVC上使用内联asm的整个前提可能是一个稻草人的论点。但它确实向您展示内在函数比MSVC的内联函数要好得多。
GNU C(gcc / clang / icc)
在内联div64时,Gcc甚至比这里显示的输出更好,因为它通常可以安排前面的代码首先在edx:eax中生成64位整数。
我无法让gcc编译为32位vectorcall ABI。 Clang可以,但它在"rm"
约束的内联asm中很糟糕(在godbolt链接上尝试它:它通过内存反弹函数arg而不是在约束中使用register选项)。 64位MS调用约定接近32位向量调用,前两个参数在edx,ecx中。不同之处在于,在使用堆栈之前,还有2个参数进入了regs(并且被调用者没有弹出堆栈中的args,这就是ret 8
在MSVC输出中的含义。)
// GNU C
// change everything to int64_t to do 128b/64b -> 64b division
// MSVC doesn't do x86-64 inline asm, so we'll use 32bit to be comparable
int div64(int lo, int hi, int *premainder, int divisor) {
int quotient, rem;
asm ("idivl %[divsrc]"
: "=a" (quotient), "=d" (rem) // a means eax, d means edx
: "d" (hi), "a" (lo),
[divsrc] "rm" (divisor) // Could have just used %0 instead of naming divsrc
// note the "rm" to allow the src to be in a register or not, whatever gcc chooses.
// "rmi" would also allow an immediate, but unlike adc, idiv doesn't have an immediate form
: // no clobbers
);
*premainder = rem;
return quotient;
}