Is < faster than <=?

Is < faster than <=?

技术背景

在编程中,比较运算符 <<= 是常用的操作符。很多开发者可能会好奇,在性能方面,< 是否会比 <= 更快。这不仅关系到代码的执行效率,还涉及到编译器的优化策略以及不同硬件架构的指令执行特性。

实现步骤

1. 整数比较的汇编实现

在大多数架构(如 x86)上,整数比较通常由两条机器指令实现:

  • testcmp 指令,用于设置 EFLAGS
  • 根据比较类型和代码布局的 Jcc(跳转)指令,例如:
    • jne:不等于时跳转(ZF = 0
    • jz:等于时跳转(ZF = 1
    • jg:大于时跳转(ZF = 0 and SF = OF

以下是 C 代码及其汇编编译结果的示例:

1
2
3
4
// if (a < b)
if (a < b) {
// Do something 1
}

编译后的汇编代码:

1
2
3
4
5
mov     eax, DWORD PTR [esp+24]      ; a
cmp eax, DWORD PTR [esp+28] ; b
jge .L2 ; jump if a is >= b
; Do something 1
.L2:
1
2
3
4
// if (a <= b)
if (a <= b) {
// Do something 2
}

编译后的汇编代码:

1
2
3
4
5
mov     eax, DWORD PTR [esp+24]      ; a
cmp eax, DWORD PTR [esp+28] ; b
jg .L5 ; jump if a is > b
; Do something 2
.L5:

可以看到,两者的区别仅在于 jgjge 指令,而这两条指令的执行时间是相同的。

2. 浮点比较的情况

对于 x87 浮点运算,也有类似的情况。例如:

1
int compare_strict(double a, double b) { return a < b; }

对应的汇编代码:

1
2
3
4
5
6
7
8
9
fld     QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
fstp st(0)
seta al ; Set al if above (CF=0 and ZF=0).
test al, al
je .L2
; Do something 1
.L2:
1
int compare_loose(double a, double b) { return a <= b; }

对应的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
fld     QWORD PTR [esp+32]
fld QWORD PTR [esp+40]
fucomip st, st(1) ; (same thing as above)
fstp st(0)
setae al ; Set al if above or equal (CF=0).
test al, al
je .L5
; Do something 2
.L5:
leave
ret

不过,在某些情况下,浮点的 <= 比较可能会比 < 慢一条指令。

3. 历史架构的情况

在历史上(20 世纪 80 年代和 90 年代初),某些架构中 < 可能比 <= 快。这是因为整数比较通常通过整数减法实现,例如:

  • A < B 等价于 A - B < 0
  • A = B 等价于 A - B = 0
  • A > B 等价于 A - B > 0

在这些架构中,实现 < 比较可能只需一条指令,而 <= 比较可能需要额外检查零标志。但在现代架构中,这种差异几乎可以忽略不计。

4. 编译器优化与循环条件

在 C++ 中,编译器遵循“as-if”规则。如果 a < 901a <= 900 结果相同,编译器会选择更优的实现。但在循环条件中,<= 可能会影响编译器的优化。例如:

1
2
3
4
5
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
}

size = 0 时,upper_bound = UINT_MAX,循环会变成无限循环,编译器可能无法进行激进的优化。

核心代码

以下是 C++ 中不同比较运算符在循环中的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 < 的循环
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}

// 使用 <= 的循环
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}

最佳实践

  • 一般情况下,无需过于纠结 <<= 的性能差异,因为编译器会进行优化。
  • 在编写循环条件时,尽量避免使用 <= 导致编译器无法证明循环是否会终止,从而影响优化效果。
  • 对于特定架构的优化,应进行实际测试,避免盲目进行微优化。

常见问题

1. 在所有架构中 < 都比 <= 快吗?

不是的。在大多数现代架构中,两者的性能差异可以忽略不计。但在某些特殊架构或特定情况下,<= 可能会稍慢。

2. 编译器会如何处理 <<= 的比较?

编译器会根据“as-if”规则,选择最适合的实现方式。如果两种比较结果相同,编译器会选择更高效的版本。

3. 循环条件中使用 <= 会有什么影响?

在某些情况下,使用 <= 可能会阻止编译器证明循环是否会终止,从而影响自动向量化等优化。