2021-12-16 19:07 编辑:显示与 uint 和 uchar 的比较,同时显示 GCC 和 Clang。将-march=native 添加到编译器标志。现在的结果似乎表明bool 和其他整数类型一样好,但是一些编译器产生的代码不是最优的。
编辑于 2022-01-11 18:56:经过一些测试,稍微更改代码可能会显示重要的性能问题,_Bool 比 uint 更可能出现。 p>
对于我的测试,我选择了无符号类型,因为项目使用的是无符号类型而不是 bool,但我希望有符号类型的行为类似。
我将在此处展示使用 unsigned char 进行的测试,因为 bool 在我的系统中是 1 个字节,这减少了汇编输出的差异,并且还使用 unsigned int 比较不同的宽度。
我测试了将整数存储到其中一种类型(bool、unsigned char 和 unsigned int)中,使用其中一种类型来控制选择语句,并使用其中一种类型作为参数功能。
源代码:
// repeat.h:
#pragma once
#define repeat2(e) (e);(e)
#define repeat4(e) repeat2(e);repeat2(e)
#define repeat8(e) repeat4(e);repeat4(e)
#define repeat16(e) repeat8(e);repeat8(e)
#define repeat32(e) repeat16(e);repeat16(e)
#define repeat64(e) repeat32(e);repeat32(e)
#define repeat128(e) repeat64(e);repeat64(e)
#define repeat256(e) repeat128(e);repeat128(e)
#define repeat512(e) repeat256(e);repeat256(e)
#define repeat1024(e) repeat512(e);repeat512(e)
#define repeat(e) do \
{ \
repeat16(e); \
} while (0)
// store_bool.h:
#pragma once
_Bool store_bool(long n, int x);
// store_bool.c:
#include "store_bool.h"
#include "repeat.h"
_Bool store_bool(long n, volatile int x)
{
volatile _Bool b;
for (long i = 0; i < n; i++)
repeat(b = x);
return b;
}
// store_uchar.h:
#pragma once
unsigned char store_uchar(long n, int x);
// store_uchar.c:
#include "store_uchar.h"
#include "repeat.h"
unsigned char store_uchar(long n, volatile int x)
{
volatile unsigned char c;
for (long i = 0; i < n; i++)
repeat(c = x);
return c;
}
// store_uint.h:
#pragma once
unsigned int store_uint(long n, int x);
// store_uint.c:
#include "store_uint.h"
#include "repeat.h"
unsigned int store_uint(long n, volatile int x)
{
volatile unsigned int u;
for (long i = 0; i < n; i++)
repeat(u = x);
return u;
}
// consume_bool.h:
#pragma once
int consume_bool(long n, _Bool b);
// consume_bool.c:
#include "consume_bool.h"
#include "repeat.h"
int consume_bool(long n, volatile _Bool b)
{
volatile int x = 5;
for (long i = 0; i < n; i++)
repeat({if (b) x = 3;});
return x;
}
// consume_uchar.h:
#pragma once
int consume_uchar(long n, unsigned char u);
// consume_uchar.c:
#include "consume_uchar.h"
#include "repeat.h"
int consume_uchar(long n, volatile unsigned char c)
{
volatile int x = 5;
for (long i = 0; i < n; i++)
repeat({if (c) x = 3;});
return x;
}
// consume_uint.h:
#pragma once
int consume_uint(long n, unsigned int u);
// consume_uint.c:
#include "consume_uint.h"
#include "repeat.h"
int consume_uint(long n, volatile unsigned int u)
{
volatile int x = 5;
for (long i = 0; i < n; i++)
repeat({if (u) x = 3;});
return x;
}
// param_bool_.h:
#pragma once
int param_bool_(_Bool x);
// param_bool_.c:
#include "param_bool_.h"
int param_bool_(_Bool b)
{
return b ? 3 : 5;
}
// param_bool.h:
#pragma once
void param_bool(long n, _Bool b);
// param_bool.c:
#include "param_bool.h"
#include "param_bool_.h"
#include "repeat.h"
void param_bool(long n, volatile _Bool b)
{
for (long i = 0; i < n; i++)
repeat(param_bool_(b));
}
// param_uchar_.h:
#pragma once
int param_uchar_(unsigned char c);
// param_uchar_.c:
#include "param_uchar_.h"
int param_uchar_(unsigned char c)
{
return c ? 3 : 5;
}
// param_uchar.h:
#pragma once
void param_uchar(long n, unsigned char c);
// param_uchar.c:
#include "param_uchar.h"
#include "param_uchar_.h"
#include "repeat.h"
void param_uchar(long n, volatile unsigned char c)
{
for (long i = 0; i < n; i++)
repeat(param_bool_(c));
}
// param_uint_.h:
#pragma once
int param_uint_(unsigned int u);
// param_uint_.c:
#include "param_uint_.h"
int param_uint_(unsigned int u)
{
return u ? 3 : 5;
}
// param_uint.h:
#pragma once
void param_uint(long n, unsigned int u);
// param_uint.c:
#include "param_uint.h"
#include "param_uint_.h"
#include "repeat.h"
void param_uint(long n, volatile unsigned int u)
{
for (long i = 0; i < n; i++)
repeat(param_bool_(u));
}
// main.c:
#include <stdio.h>
#include <time.h>
#include "store_bool.h"
#include "store_uchar.h"
#include "store_uint.h"
#include "consume_bool.h"
#include "consume_uchar.h"
#include "consume_uint.h"
#include "param_bool.h"
#include "param_uchar.h"
#include "param_uint.h"
#define measure(e) \
({ \
clock_t t0, t1; \
double t; \
\
t0 = clock(); \
e; \
t1 = clock(); \
\
t = (double) (t1 - t0) / CLOCKS_PER_SEC; \
t; \
})
int main(int argc, char *argv[])
{
double sb, sc, su;
double cb, cc, cu;
double pb, pc, pu;
long n;
if (argc != 2)
exit(2);
n = atol(argv[1]);
sb = measure(store_bool(n, 1));
sc = measure(store_uchar(n, 1));
su = measure(store_uint(n, 1));
cb = measure(consume_bool(n, 1));
cc = measure(consume_uchar(n, 1));
cu = measure(consume_uint(n, 1));
pb = measure(param_bool(n, 1));
pc = measure(param_uchar(n, 1));
pu = measure(param_uint(n, 1));
printf("n: %li\n", n);
putchar('\n');
printf("store bool: %lf\n", sb);
printf("store uchar: %lf\n", sc);
printf("store uint: %lf\n", su);
putchar('\n');
printf("consume bool: %lf\n", cb);
printf("consume uchar: %lf\n", cc);
printf("consume uint: %lf\n", cu);
putchar('\n');
printf("param bool: %lf\n", pb);
printf("param uchar: %lf\n", pc);
printf("param uint: %lf\n", pu);
}
我对一些变量使用了volatile,以避免编译器优化出多个赋值和测试。
由于编译器不会展开循环,因为它们很大,我在每个循环中使用了许多 (16) 个重复表达式(请参阅 repeat() 宏),以减少循环开销(跳转指令)的影响总基准测试时间。
编译:
$ cc -Wall -Wextra -O3 -march=native -S *.c
$ cc -O3 -march=native *.s
$
组装:
为了简化,我将在 16 次重复中挑选一个。如果你想查看完整的汇编文件,你可以自己编译(我在这里给出了足够的说明)。
// store_bool.s (GCC):
movl -20(%rsp), %edx
testl %edx, %edx
setne %dl
movb %dl, -1(%rsp)
// store_bool.s (Clang):
cmpl $0, -4(%rsp)
setne -5(%rsp)
// sotre_uchar.s (GCC):
movl -20(%rsp), %edx
movb %dl, -1(%rsp)
// store_uchar.s (Clang):
movl -4(%rsp), %ecx
movb %cl, -5(%rsp)
// store_uint.s (GCC):
movl -20(%rsp), %edx
movl %edx, -4(%rsp)
// store_uint.s (Clang):
movl -4(%rsp), %ecx
movl %ecx, -8(%rsp)
从上面看,uchar 和 uint 很可能是相同的。 bool 在 Clang 上也有两条指令,但它们是不同的;这可能会或可能不会有所作为。在 GCC 上,与 uchar 相比,它显然有 2 个额外的指令,这使得它更慢。
// consume_bool.s (GCC):
movzbl -20(%rsp), %edx
testb %dl, %dl
je .L2
movl $3, -4(%rsp)
.L2:
// consume_bool.s (Clang):
.LBB0_5: # in Loop: Header=BB0_1 Depth=1
testb $1, -5(%rsp)
jne .LBB0_6
[...]
.LBB0_6: # in Loop: Header=BB0_1 Depth=1
movl $3, -4(%rsp)
testb $1, -5(%rsp)
je .LBB0_9
(LBB0_9 与 LBB0_5 类似)
// consume_uchar.s (GCC):
movzbl -20(%rsp), %edx
testb %dl, %dl
je .L2
movl $3, -4(%rsp)
.L2:
// consume_uchar.s (Clang):
cmpb $0, -5(%rsp)
je .LBB0_3
# %bb.2: # in Loop: Header=BB0_1 Depth=1
movl $3, -4(%rsp)
.LBB0_3: # in Loop: Header=BB0_1 Depth=1
// consume_uint.s (GCC):
movl -20(%rsp), %edx
testl %edx, %edx
je .L2
movl $3, -4(%rsp)
.L2:
// consume_uint.s (Clang):
cmpl $0, -4(%rsp)
je .LBB0_3
# %bb.2: # in Loop: Header=BB0_1 Depth=1
movl $3, -8(%rsp)
.LBB0_3: # in Loop: Header=BB0_1 Depth=1
在这些情况下,GCC 生成的程序集对于 3 种类型几乎相同,所以我不认为有任何区别。在 Clang 中,bool 有不同的代码,但由于它非常不同,因此很难预测它会比整数快还是慢。
// param_bool_.s (GCC):
param_bool_:
.LFB0:
.cfi_startproc
cmpb $1, %dil
sbbl %eax, %eax
andl $2, %eax
addl $3, %eax
ret
.cfi_endproc
.LFE0:
// param_bool_.s (Clang):
param_bool_: # @param_bool_
.cfi_startproc
# %bb.0:
xorb $1, %dil
movzbl %dil, %eax
addl %eax, %eax
addl $3, %eax
retq
.Lfunc_end0:
// param_bool.s (GCC):
movzbl 12(%rsp), %edi
call param_bool_@PLT
// param_bool.s (Clang):
movzbl 15(%rsp), %edi
andl $1, %edi
callq param_bool_
// param_uchar_.s (GCC):
param_uchar_:
.LFB0:
.cfi_startproc
cmpb $1, %dil
sbbl %eax, %eax
andl $2, %eax
addl $3, %eax
ret
.cfi_endproc
.LFE0:
// param_uchar_.s (Clang):
param_uchar_: # @param_uchar_
.cfi_startproc
# %bb.0:
xorl %eax, %eax
testl %edi, %edi
sete %al
addl %eax, %eax
addl $3, %eax
retq
.Lfunc_end0:
// param_uchar.s (GCC):
movzbl 12(%rsp), %edi
call param_uchar_@PLT
// param_uchar.s (Clang):
movzbl 15(%rsp), %edi
callq param_uchar_
// param_uint_.s (GCC):
param_uint_:
.LFB0:
.cfi_startproc
cmpl $1, %edi
sbbl %eax, %eax
andl $2, %eax
addl $3, %eax
ret
.cfi_endproc
.LFE0:
// param_uint_.s (Clang):
param_uint_: # @param_uint_
.cfi_startproc
# %bb.0:
xorl %eax, %eax
testl %edi, %edi
sete %al
addl %eax, %eax
addl $3, %eax
retq
.Lfunc_end0:
// param_uint.s (GCC):
movl 12(%rsp), %edi
call param_uint_@PLT
// param_uint.s (Clang):
movl 12(%rsp), %edi
callq param_uint_
在这种情况下,bool 应该与 uchar 相同,因为唯一重要的应该是宽度,我们可能会看到(或看不到)与 uint 的差异。从零扩展的一部分,没有太大的区别。 GCC 和 Clang 之间存在细微差别,但是 Clang 生成的代码更大,所以我预计 Clang 的运行速度会比 GCC 稍慢。
时间:
// amd64, gcc-11, i5-5675C:
$ ./a.out 1073741824
store bool: 4.928789
store uchar: 4.795028
store uint: 4.803893
consume bool: 4.795776
consume uchar: 4.794873
consume uint: 4.794079
param bool: 17.713958
param uchar: 17.611229
param uint: 17.688909
// amd64, clang-13, i5-5675C:
$ ./a.out 1073741824
store bool: 4.806418
store uchar: 4.802943
store uint: 4.800172
consume bool: 4.805537
consume uchar: 4.799858
consume uint: 4.799462
param bool: 19.095543
param uchar: 17.708014
param uint: 17.782490
在“存储”中,正如我们预期的那样,bool 比使用 GCC 的其他类型慢(大约 1~10%)。使用 Clang,没有显着差异(我看到 bool 一直比其他人慢一点,但不到 0.5%)。
在“消费”中,我们看不到类型或编译器之间的区别。
在 'param' 中,运行之间的时间变化很大,并且没有一致性:有时 bool 更慢,有时更快。但是,GCC 始终比 Clang 快。
代码中的微小更改可能会导致编译器缺少重要的优化。在consume_<type>.c 中使用以下代码会导致一些重要的性能损失:
repeat(x = b ? 3 : x);
请注意,仅将 if 更改为三元运算符,会使编译器减速到以下时间:
海合会:
$ ./a.out 1073741824
n: 1073741824
...
consume bool: 8.684662
consume uchar: 8.683915
consume uint: 8.086806
...
叮当声:
$ ./a.out 1073741824
n: 1073741824
...
consume bool: 8.161896
consume uchar: 5.422896
consume uint: 5.127165
...
_Bool 的 Clang 速度明显减慢,而其他类型的速度保持合理。 GCC 似乎为所有类型生成了非常糟糕的代码。
结论:
程序员应该考虑以下几点:
性能:尽管理论上 _Bool 可能与 unsigned int 一样快,但编译器远非理想,而且您的编译器很可能会错过一些优化,这在某些情况下可能非常重要。
可维护性/可读性/正确性:有些人可能认为_Bool 由于自动规范化而更安全;其他人可能会争辩说,由于自动归一化,它不太安全;只知道你在使用什么,并形成你自己的意见。
支持 C99 之前的代码:如果是这种情况,您别无选择,只能使用 unsigned int。