我已经阅读了各种优化指南,声称ADD 1比在x86中使用INC更快.这是真的吗?
在MSVC 2013中编译以下代码,64位版本构建,/O2优化:
while (*s == ' ' || *s == ',' || *s == '\r' || *s == '\n') {
++s;
}
Run Code Online (Sandbox Code Playgroud)
我得到了以下代码 - 使用64位寄存器作为带有bt(位测试)指令的查找表,它具有非常酷的优化.
mov rcx, 17596481020928 ; 0000100100002400H
npad 5
$LL82@myFunc:
movzx eax, BYTE PTR [rsi]
cmp al, 44 ; 0000002cH
ja SHORT $LN81@myFunc
movsx rax, al
bt rcx, rax
jae SHORT $LN81@myFunc
inc rsi
jmp SHORT $LL82@myFunc
$LN81@myFunc:
; code after loop...
Run Code Online (Sandbox Code Playgroud)
但我的问题是:movsx rax, al第一个分支后的目的是什么?
首先,我们从字符串中加载一个字节rax并对其进行零扩展:
movzx eax, BYTE …Run Code Online (Sandbox Code Playgroud) 我在Delphi中编写一个简单的BigInteger类型.它主要由TLimb的动态数组组成,其中TLimb是32位无符号整数,32位大小字段,它还保存BigInteger的符号位.
要添加两个BigIntegers,我创建一个适当大小的新BigInteger然后,在一些簿记之后,调用以下过程,将三个指针传递给左右操作数和结果的数组的相应开始,以及左右肢的数量分别为.
普通代码:
class procedure BigInteger.PlainAdd(Left, Right, Result: PLimb; LSize, RSize: Integer);
asm
// EAX = Left, EDX = Right, ECX = Result
PUSH ESI
PUSH EDI
PUSH EBX
MOV ESI,EAX // Left
MOV EDI,EDX // Right
MOV EBX,ECX // Result
MOV ECX,RSize // Number of limbs at Left
MOV EDX,LSize // Number of limbs at Right
CMP EDX,ECX
JAE @SkipSwap
XCHG ECX,EDX // Left and LSize should be largest
XCHG ESI,EDI // so swap
@SkipSwap:
SUB EDX,ECX // …Run Code Online (Sandbox Code Playgroud) 多年来我多次读过你应该做XOR ax,ax因为它更快......或者用C编程时使用counter ++或counter + = 1因为它们会INC或ADD ......或者在Netburst Pentium中4 INC比ADD 1慢,因此必须警告编译器你的目标是一个Netburst所以它会将所有var ++翻译成ADD 1 ......
我的问题是:为什么INC和ADD有不同的表现?为什么例如INC声称在Netburst上速度较慢而在其他处理器中比ADD快?
我正在看一些通过反汇编一些C程序生成的程序集,我对我经常重复看到的单个优化感到困惑.
当我没有对GCC编译器进行优化时使用subl减法指令,但是当我确实打开了优化(-O3准确地说)时,编译器使用leal指令而不是减法,例如:
没有优化:
83 e8 01 subl $0x1, %eax
Run Code Online (Sandbox Code Playgroud)
优化
8d 6f ff leal -0x1(%edi), %ebp
Run Code Online (Sandbox Code Playgroud)
这两条指令都是3个字节长,所以我没有在这里看到优化.有人可以帮助我并尝试解释编译器的选择吗?
任何帮助,将不胜感激.
我读过的所有内容似乎都表明分支错误预测总会导致整个管道被刷新,这意味着浪费了很多周期.我从来没有听到任何人提到短期if条件的任何例外情况.
这似乎在某些情况下会非常浪费.例如,假设您有一个单独的if语句,其中包含一个非常简单的主体,该主体被编译为1个CPU指令.if子句将被编译为一条指令的条件跳转.如果CPU预测分支不被采用,则它将开始执行if-body指令,并可立即开始执行以下指令.现在,一旦if条件的评估已经到达管道的末端,也就是说,例如,12个周期之后,CPU现在知道它的预测是对还是错.如果它被错误预测,并且分支实际被占用,则CPU实际上只需要丢弃来自管道的1条指令(if-body中的指令).但是,如果它刷新整个管道,那么在以下指令中完成的所有工作也都被浪费了,并且必须无缘无故地重复.这是一个深度流水线架构上浪费的大量周期.
那么现代CPU有没有任何机制可以只丢弃短if体内的少数指令?或者它真的冲洗整个管道?如果是后者,那么我认为使用条件移动指令会获得更好的性能.顺便说一下,有没有人知道现代编译器是否善于将短if语句转换为cmov指令?
从这里我知道英特尔近年来实施了几种静态分支预测机制:
80486年龄:永远不被采取
Pentium4年龄:未采取后退/前锋
像Ivy Bridge,Haswell这样的新型CPU变得越来越无形,请参阅Matt G的实验.
英特尔似乎不想再谈论它,因为我在英特尔文档中找到的最新资料大约是十年前写的.
我知道静态分支预测(远远不是)比动态更重要,但在很多情况下,CPU将完全丢失,程序员(使用编译器)通常是最好的指南.当然,这些情况通常不是性能瓶颈,因为一旦频繁执行分支,动态预测器就会捕获它.
由于英特尔不再在其文档中明确声明动态预测机制,因此GCC的builtin_expect()只能从热路径中删除不太可能的分支.
我不熟悉CPU的设计,我不知道究竟是什么机制,目前英特尔使用其静态预测,但我还是觉得英特尔的最佳机制应该清楚地记录他的CPU",我打算去当动态预测失败,向前或向后',因为通常程序员是当时最好的指南.
更新:
我发现你提到的主题逐渐超出我的知识范围.这里涉及一些动态预测机制和CPU内部细节,我在两三天内无法学习.所以请允许我暂时退出你的讨论并充电.
这里仍然欢迎任何答案,也许会帮助更多人
compiler-construction x86 intel cpu-architecture branch-prediction
有些人在需要没有计数器或计数器的循环时编写这样的代码n-1, ..., 0:
while (i--) { ... }
Run Code Online (Sandbox Code Playgroud)
一个具体的例子:
volatile int sink;
void countdown_i_used() {
unsigned i = 1000;
while (i--) {
sink = i; // if i is unused, gcc optimizes it away and uses dec/jnz
}
}
Run Code Online (Sandbox Code Playgroud)
在GCC 8.2(在Godbolt编译器资源管理器上),它被编译成
# gcc8.2 -O3 -march=haswell
.L2:
mov DWORD PTR sink[rip], eax
dec eax # with tune=generic, sub eax, 1
cmp eax, -1
jne .L2
Run Code Online (Sandbox Code Playgroud)
在clang(https://godbolt.org/z/YxYZ95)上,如果不使用计数器,它会变成
if(i) do {...} while(--i);
Run Code Online (Sandbox Code Playgroud)
但如果使用,就像GCC一样
add esi, -1
cmp …Run Code Online (Sandbox Code Playgroud) 我注意到Clang为以下片段做了一个有趣的部门优化技巧
int64_t s2(int64_t a, int64_t b)
{
return a/b;
}
Run Code Online (Sandbox Code Playgroud)
如果指定march为Sandy Bridge或更高版本,则下面是装配输出
mov rax, rdi
mov rcx, rdi
or rcx, rsi
shr rcx, 32
je .LBB1_1
cqo
idiv rsi
ret
.LBB1_1:
xor edx, edx
div esi
ret
Run Code Online (Sandbox Code Playgroud)
根据我的理解,它检查两个操作数的高位是否为零,如果是真的则进行32位除法
我检查了这个表,看到Core2和Nehalem上32/64位分区的延迟分别为40/116和26/89.因此,如果操作数确实通常不宽,那么通过执行32位除法而不是64位除法的节省可能与SnB一样值得
那么为什么它仅适用于SnB和后来的微体系结构?为什么GCC或ICC等其他编译器不这样做呢?