三元组还是 if 语句更快?
Are ternaries or if statements faster?
所以我有两个选择,两个函数的类型相同:
(Entry->d_type == DT_DIR ? rmdirr : remove)(CurrentEntryPath);
或
if (Entry->d_type == DT_DIR) {
rmdirr(CurrentEntryPath);
} else {
remove(CurrentEntryPath);
}
我已经确认三进制是 %100% 安全的,因为这两个函数都是兼容的指针类型。哪个更快(即使可读性较差)?
很难判断什么实际上更有效率。 if-else 产生的指令较少,但如果不满足分支预测,则有一条分支指令需要流水线刷新。
#define SOMEVALUE 5
int __attribute__((noinline)) foo(int x)
{
return rand();
}
int __attribute__((noinline)) boo(int x)
{
return rand();
}
int aaa(int x)
{
int result;
if(x == 5)
result = foo(x);
else
result = boo(x);
return result;
}
int bbb(int x)
{
int result;
return (x == 5 ? foo : boo)(x);
}
int (*z[2])(int) = {foo, boo};
int ccc(int x)
{
return z[!!(x == 5)](x);
}
以及生成的代码:
foo:
jmp rand
boo:
jmp rand
aaa:
cmp edi, 5
je .L6
jmp boo
.L6:
jmp foo
bbb:
cmp edi, 5
mov eax, OFFSET FLAT:foo
mov edx, OFFSET FLAT:boo
cmovne rax, rdx
jmp rax
ccc:
xor eax, eax
cmp edi, 5
sete al
jmp [QWORD PTR z[0+rax*8]]
z:
.quad foo
.quad boo
在我看来,如果您在不那么琐碎的代码中进行这样的微优化 - 您需要查看生成的代码并决定什么更有效。
很可能优化编译器会生成相同的
两种情况的代码。
奇怪的是 gcc 和 clang 在这种情况下不这样做而是生成
:?
情况下字面上使用函数指针的代码
第二种情况直接跳转。
示例:
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>
int rmitem0(struct dirent const*Entry)
{
return (Entry->d_type == DT_DIR ? rmdir : remove)(Entry->d_name);
}
int rmitem1(struct dirent const*Entry)
{
if (Entry->d_type == DT_DIR)
return rmdir(Entry->d_name);
else return remove(Entry->d_name);
}
x86_64 铿锵声:
0000000000000000 <rmitem0>:
0: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
4: b8 00 00 00 00 mov eax,0x0 5: R_X86_64_32 rmdir
9: b9 00 00 00 00 mov ecx,0x0 a: R_X86_64_32 remove
e: 48 0f 44 c8 cmove rcx,rax
12: 48 83 c7 13 add rdi,0x13
16: ff e1 jmp rcx
0000000000000018 <rmitem1>:
18: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
1c: 48 8d 7f 13 lea rdi,[rdi+0x13]
20: 0f 85 00 00 00 00 jne 26 <rmitem1+0xe> 22: R_X86_64_PLT32 remove-0x4
26: e9 00 00 00 00 jmp 2b <rmitem1+0x13> 27: R_X86_64_PLT32 rmdir-0x4
x86_64 gcc:
0000000000000000 <rmitem0>:
0: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
4: 74 09 je f <rmitem0+0xf>
6: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] # d <rmitem0+0xd> 9: R_X86_64_REX_GOTPCRELX remove-0x4
d: eb 07 jmp 16 <rmitem0+0x16>
f: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] # 16 <rmitem0+0x16> 12: R_X86_64_REX_GOTPCRELX rmdir-0x4
16: 48 83 c7 13 add rdi,0x13
1a: ff e0 jmp rax
000000000000001c <rmitem1>:
1c: 4c 8d 47 13 lea r8,[rdi+0x13]
20: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
24: 4c 89 c7 mov rdi,r8
27: 75 05 jne 2e <rmitem1+0x12>
29: e9 00 00 00 00 jmp 2e <rmitem1+0x12> 2a: R_X86_64_PLT32 rmdir-0x4
2e: e9 00 00 00 00 jmp 33 <rmitem1+0x17> 2f: R_X86_64_PLT32 remove-0x4
因此,这两种策略在这里的性能特征应该略有不同,但无论如何,你只见一棵小树,不见森林。
我在 Linux 上测得 rmdir
的持续时间约为 14µs。
上面的条件应该只需要一小部分 ns,最多几 ns:这比你的瓶颈快 10,000 多倍。
规则 #0 - 不要考虑原始速度;相反,请考虑 "which would I rather fix 8 months from now when someone reports a bug".
规则 #1 - 衡量,不要猜测,也不要让无法访问您的系统的人猜测。在目标系统上编写两个版本并对它们进行分析 - 检查生成的机器代码,并且 运行 每个版本都针对足够大的测试集以生成可用的统计信息并分析结果。考虑一下它是如何使用的——它是在一个紧密的循环中被调用数千次,还是在程序的整个生命周期中被调用一次?每个函数都涉及更新文件系统,与决定调用哪个函数相比,这将花费更多数量级的时间来执行 无论您使用哪种方法。
规则 #2 - 如果你的代码给了你错误的答案,或者做了错误的事情,或者将你的信用卡信息暴露给了世界,或者如果有人在隔壁房间打喷嚏,或者没有人(包括您自己)可以修复或更新它。代码的正确性首先,其次是可读性和可维护性,其次是安全性和可靠性,然后是速度。大部分显着的速度提升来自使用正确的算法和数据结构,而不是您选择的流量控制结构。
规则 #3 - 不要使用三元运算符代替 if-else
结构 只是 进行流量控制;那不是它的工作。虽然第一个版本有效,但它有点刺眼且难以一目了然,当你从现在起六个月后重新拿起它时,你会问自己为什么这样做。而且我几乎可以保证它不会比其他方法更快或更慢。
我并不是说速度不重要 - 我是说速度只是需要考虑的一件事,除非您在特定领域工作,否则它不是最重要的事情。
所以我有两个选择,两个函数的类型相同:
(Entry->d_type == DT_DIR ? rmdirr : remove)(CurrentEntryPath);
或
if (Entry->d_type == DT_DIR) {
rmdirr(CurrentEntryPath);
} else {
remove(CurrentEntryPath);
}
我已经确认三进制是 %100% 安全的,因为这两个函数都是兼容的指针类型。哪个更快(即使可读性较差)?
很难判断什么实际上更有效率。 if-else 产生的指令较少,但如果不满足分支预测,则有一条分支指令需要流水线刷新。
#define SOMEVALUE 5
int __attribute__((noinline)) foo(int x)
{
return rand();
}
int __attribute__((noinline)) boo(int x)
{
return rand();
}
int aaa(int x)
{
int result;
if(x == 5)
result = foo(x);
else
result = boo(x);
return result;
}
int bbb(int x)
{
int result;
return (x == 5 ? foo : boo)(x);
}
int (*z[2])(int) = {foo, boo};
int ccc(int x)
{
return z[!!(x == 5)](x);
}
以及生成的代码:
foo:
jmp rand
boo:
jmp rand
aaa:
cmp edi, 5
je .L6
jmp boo
.L6:
jmp foo
bbb:
cmp edi, 5
mov eax, OFFSET FLAT:foo
mov edx, OFFSET FLAT:boo
cmovne rax, rdx
jmp rax
ccc:
xor eax, eax
cmp edi, 5
sete al
jmp [QWORD PTR z[0+rax*8]]
z:
.quad foo
.quad boo
在我看来,如果您在不那么琐碎的代码中进行这样的微优化 - 您需要查看生成的代码并决定什么更有效。
很可能优化编译器会生成相同的 两种情况的代码。
奇怪的是 gcc 和 clang 在这种情况下不这样做而是生成
:?
情况下字面上使用函数指针的代码
第二种情况直接跳转。
示例:
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>
int rmitem0(struct dirent const*Entry)
{
return (Entry->d_type == DT_DIR ? rmdir : remove)(Entry->d_name);
}
int rmitem1(struct dirent const*Entry)
{
if (Entry->d_type == DT_DIR)
return rmdir(Entry->d_name);
else return remove(Entry->d_name);
}
x86_64 铿锵声:
0000000000000000 <rmitem0>:
0: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
4: b8 00 00 00 00 mov eax,0x0 5: R_X86_64_32 rmdir
9: b9 00 00 00 00 mov ecx,0x0 a: R_X86_64_32 remove
e: 48 0f 44 c8 cmove rcx,rax
12: 48 83 c7 13 add rdi,0x13
16: ff e1 jmp rcx
0000000000000018 <rmitem1>:
18: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
1c: 48 8d 7f 13 lea rdi,[rdi+0x13]
20: 0f 85 00 00 00 00 jne 26 <rmitem1+0xe> 22: R_X86_64_PLT32 remove-0x4
26: e9 00 00 00 00 jmp 2b <rmitem1+0x13> 27: R_X86_64_PLT32 rmdir-0x4
x86_64 gcc:
0000000000000000 <rmitem0>:
0: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
4: 74 09 je f <rmitem0+0xf>
6: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] # d <rmitem0+0xd> 9: R_X86_64_REX_GOTPCRELX remove-0x4
d: eb 07 jmp 16 <rmitem0+0x16>
f: 48 8b 05 00 00 00 00 mov rax,QWORD PTR [rip+0x0] # 16 <rmitem0+0x16> 12: R_X86_64_REX_GOTPCRELX rmdir-0x4
16: 48 83 c7 13 add rdi,0x13
1a: ff e0 jmp rax
000000000000001c <rmitem1>:
1c: 4c 8d 47 13 lea r8,[rdi+0x13]
20: 80 7f 12 04 cmp BYTE PTR [rdi+0x12],0x4
24: 4c 89 c7 mov rdi,r8
27: 75 05 jne 2e <rmitem1+0x12>
29: e9 00 00 00 00 jmp 2e <rmitem1+0x12> 2a: R_X86_64_PLT32 rmdir-0x4
2e: e9 00 00 00 00 jmp 33 <rmitem1+0x17> 2f: R_X86_64_PLT32 remove-0x4
因此,这两种策略在这里的性能特征应该略有不同,但无论如何,你只见一棵小树,不见森林。
我在 Linux 上测得 rmdir
的持续时间约为 14µs。
上面的条件应该只需要一小部分 ns,最多几 ns:这比你的瓶颈快 10,000 多倍。
规则 #0 - 不要考虑原始速度;相反,请考虑 "which would I rather fix 8 months from now when someone reports a bug".
规则 #1 - 衡量,不要猜测,也不要让无法访问您的系统的人猜测。在目标系统上编写两个版本并对它们进行分析 - 检查生成的机器代码,并且 运行 每个版本都针对足够大的测试集以生成可用的统计信息并分析结果。考虑一下它是如何使用的——它是在一个紧密的循环中被调用数千次,还是在程序的整个生命周期中被调用一次?每个函数都涉及更新文件系统,与决定调用哪个函数相比,这将花费更多数量级的时间来执行 无论您使用哪种方法。
规则 #2 - 如果你的代码给了你错误的答案,或者做了错误的事情,或者将你的信用卡信息暴露给了世界,或者如果有人在隔壁房间打喷嚏,或者没有人(包括您自己)可以修复或更新它。代码的正确性首先,其次是可读性和可维护性,其次是安全性和可靠性,然后是速度。大部分显着的速度提升来自使用正确的算法和数据结构,而不是您选择的流量控制结构。
规则 #3 - 不要使用三元运算符代替 if-else
结构 只是 进行流量控制;那不是它的工作。虽然第一个版本有效,但它有点刺眼且难以一目了然,当你从现在起六个月后重新拿起它时,你会问自己为什么这样做。而且我几乎可以保证它不会比其他方法更快或更慢。
我并不是说速度不重要 - 我是说速度只是需要考虑的一件事,除非您在特定领域工作,否则它不是最重要的事情。