这个问题显然在不同的系统上有不同的答案,从这个意义上说,它从一开始就是病态的。例如,i486 没有流水线,而奔腾没有 SSE。
要问的正确问题是:“ 最快的方法是什么?
在 X 系统中将 single char hex 转换为 dec,例如i686 " .
在此处的方法中,在具有多级流水线的系统上,这个问题的答案实际上是相同或非常非常非常几乎相同的。任何没有管道的系统都会倾向于查找表方法 (LUT),但如果内存访问速度较慢,则条件方法 (CEV) 或按位求值方法 (BEV) 可能会受益,具体取决于 xor 与负载的速度。给定 CPU。
(CEV) 将寄存器which is not prone to mis-prediction 的比较和条件移动分解为2 个加载有效地址。所有这些命令都可以在奔腾管道中配对。所以它们实际上是在 1 个周期中进行的。
8d 57 d0 lea -0x30(%rdi),%edx
83 ff 39 cmp $0x39,%edi
8d 47 a9 lea -0x57(%rdi),%eax
0f 4e c2 cmovle %edx,%eax
(LUT)分解为寄存器之间的 mov 和来自数据相关内存位置的 mov 加上一些用于对齐的 nop,并且应该至少占用 1 个周期。和前面一样,只有数据依赖关系。
48 63 ff movslq %edi,%rdi
8b 04 bd 00 1d 40 00 mov 0x401d00(,%rdi,4),%eax
(BEV)是一个不同的野兽,因为它实际上需要 2 movs + 2 xors + 1 和一个条件 mov。这些也可以很好地流水线化。
89 fa mov %edi,%edx
89 f8 mov %edi,%eax
83 f2 57 xor $0x57,%edx
83 f0 30 xor $0x30,%eax
83 e7 40 and $0x40,%edi
0f 45 c2 cmovne %edx,%eax
当然,将只是一个符号字符转换为对应用程序至关重要(也许火星探路者是候选者)是非常罕见的情况。相反,人们会期望通过实际创建一个循环并调用该函数来转换一个更大的字符串。
因此,在这种情况下,矢量化程度更高的代码是赢家。 LUT 没有矢量化,BEV 和 CEV 具有更好的性能。 一般来说,这样的微优化不会让你到任何地方,编写你的代码并让它活着(即让编译器运行)。
所以我实际上已经在这个意义上构建了一些测试,这些测试在任何具有 c++11 编译器和随机设备源的系统上易于重现,例如任何 *nix 系统。如果不允许矢量化,-O2 CEV/LUT 几乎相等,但是一旦设置了-O3,编写更可分解的代码的优势就会显示出差异。
总而言之,如果您有旧的编译器,请使用 LUT,如果
您的系统是低端或旧的考虑 BEV,否则编译器
会比你聪明,你应该使用 CEV。
问题:问题是从字符集 {0,1,2,3,4,5,6,7,8,9,a,b,c,d ,e,f} 到 {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15} 的集合。没有考虑大写字母。
这个想法是在分段中利用 ascii 表的线性。
[简单易行]:条件评估 -> CEV
int decfromhex(int const x)
{
return x<58?x-48:x-87;
}
[Dirty and complex]:按位求值 -> BEV
int decfromhex(int const x)
{
return 9*(x&16)+( x & 0xf );
}
[编译时间]:模板条件评估 -> TCV
template<char n> int decfromhex()
{
int constexpr x = n;
return x<58 ? x-48 : x -87;
}
[查找表]:查找表 -> LUT
int decfromhex(char n)
{
static int constexpr x[255]={
// fill everything with invalid, e.g. -1 except places\
// 48-57 and 97-102 where you place 0..15
};
return x[n];
}
其中,最后一个似乎是最快的乍一看。第二个只是在编译时和常量表达式。
[RESULT](请验证):*BEV 是最快的,可以处理大小写字母,但只是边缘到不处理大写字母的 CEV。随着字符串大小的增加,LUT 变得比 CEV 和 BEV 都慢。
str-sizes 16-12384 的示例结果如下所示(越低越好)
显示平均时间(100 次运行)。气泡的大小是正常误差。
The script for running the tests is available.
已对conditional CEV、bitwise BEV 和lookup table LUT 进行了一组测试随机生成的字符串。测试相当简单,来自:
Test source code
这些是可验证的:
-
输入字符串的本地副本每次都放置在本地缓冲区中。
- 保留结果的本地副本,然后将其复制到每个字符串测试的堆中
-
持续时间仅用于提取对字符串进行操作的时间
-
统一方法,没有适合其他情况的复杂机制和环绕/环绕代码。
-
无采样使用整个时序
- CPU 预热已执行
-
睡眠在测试之间发生以允许编组代码,这样一个测试就不会利用前一个测试。
-
编译使用
g++ -std=c++11 -O3 -march=native dectohex.cpp -o d2h进行
-
启动与
taskset -c 0 d2h
- 没有线程依赖或多线程
- 实际使用结果,以避免任何类型的循环优化
作为旁注,我在实践中看到版本 3 与较旧的 c++98 编译器相比要快得多。
[BOTTOM LINE]:不要害怕使用 CEV,除非你在编译时知道你的变量,你可以使用 TCV 版本。 LUT
仅应在每个用例的显着性能后使用
评估,并且可能使用较旧的编译器。另一种情况是当
您的集合更大,即 {0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,A,B,C,D,E,F}
.这也可以实现。最后,如果您对性能很感兴趣,请使用 BEV。
unordered_map 的结果已被删除,因为它们太慢而无法比较,或者最多可能与 LUT 解决方案一样快。
我的个人电脑对大小为 12384/256 的字符串和 100 个字符串的结果:
g++ -DS=2 -DSTR_SIZE=256 -DSET_SIZE=100 -DUNITS=nanoseconds -O3 -std=c++11 -march=native dectohex.cpp -o d2h && taskset -c 0 ./d2h
sign: -2709
-------------------------------------------------------------------
(CEV) Total: 185568 nanoseconds - mean: 323.98 nanoseconds error: 88.2699 nanoseconds
(BEV) Total: 185568 nanoseconds - mean: 337.68 nanoseconds error: 113.784 nanoseconds
(LUT) Total: 229612 nanoseconds - mean: 667.89 nanoseconds error: 441.824 nanoseconds
-------------------------------------------------------------------
g++ -DS=2 -DSTR_SIZE=12384 -DSET_SIZE=100 -DUNITS=nanoseconds -O3 -std=c++11 -march=native hextodec.cpp -o d2h && taskset -c 0 ./h2d
-------------------------------------------------------------------
(CEV) Total: 5539902 nanoseconds - mean: 6229.1 nanoseconds error: 1052.45 nanoseconds
(BEV) Total: 5539902 nanoseconds - mean: 5911.64 nanoseconds error: 1547.27 nanoseconds
(LUT) Total: 6346209 nanoseconds - mean: 14384.6 nanoseconds error: 1795.71 nanoseconds
-------------------------------------------------------------------
Precision: 1 ns
使用 GCC 4.9.3 编译到 metal 的系统的结果,系统没有加载到大小为 256/12384 的字符串和 100 个字符串上
g++ -DS=2 -DSTR_SIZE=256 -DSET_SIZE=100 -DUNITS=nanoseconds -O3 -std=c++11 -march=native dectohex.cpp -o d2h && taskset -c 0 ./d2h
sign: -2882
-------------------------------------------------------------------
(CEV) Total: 237449 nanoseconds - mean: 444.17 nanoseconds error: 117.337 nanoseconds
(BEV) Total: 237449 nanoseconds - mean: 413.59 nanoseconds error: 109.973 nanoseconds
(LUT) Total: 262469 nanoseconds - mean: 731.61 nanoseconds error: 11.7507 nanoseconds
-------------------------------------------------------------------
Precision: 1 ns
g++ -DS=2 -DSTR_SIZE=12384 -DSET_SIZE=100 -DUNITS=nanoseconds -O3 -std=c++11 -march=native dectohex.cpp -o d2h && taskset -c 0 ./d2h
sign: -137532
-------------------------------------------------------------------
(CEV) Total: 6834796 nanoseconds - mean: 9138.93 nanoseconds error: 144.134 nanoseconds
(BEV) Total: 6834796 nanoseconds - mean: 8588.37 nanoseconds error: 4479.47 nanoseconds
(LUT) Total: 8395700 nanoseconds - mean: 24171.1 nanoseconds error: 1600.46 nanoseconds
-------------------------------------------------------------------
Precision: 1 ns
[如何阅读结果]
平均值显示为计算给定大小的字符串所需的微秒。
给出了每次测试的总时间。平均值被计算为计算一个字符串的时间总和/总和(该区域中没有其他代码,但可以矢量化,这没关系)。误差是时间的标准偏差。
均值告诉我们平均应该期望什么,以及时间遵循常态的误差。在这种情况下,只有当它很小时,这才是一个公平的误差度量(否则我们应该使用适合正分布的东西)。在缓存未命中、处理器调度和许多其他因素的情况下,通常会出现高错误。
代码定义了一个独特的宏来运行测试,允许定义编译时变量来设置测试,并打印完整的信息,例如:
g++ -DS=2 -DSTR_SIZE=64 -DSET_SIZE=1000 -DUNITS=nanoseconds -O3 -std=c++11 -march=native dectohex.cpp -o d2h && taskset -c 0 ./d2h
sign: -6935
-------------------------------------------------------------------
(CEV) Total: 947378 nanoseconds - mean: 300.871 nanoseconds error: 442.644 nanoseconds
(BEV) Total: 947378 nanoseconds - mean: 277.866 nanoseconds error: 43.7235 nanoseconds
(LUT) Total: 1040307 nanoseconds - mean: 375.877 nanoseconds error: 14.5706 nanoseconds
-------------------------------------------------------------------
例如,要在大小为 256 的 str 上使用 2sec 暂停运行测试,总共有 10000 不同的字符串,在 double precision 中输出计时,并在 nanoseconds 中计数,以下命令编译并运行测试。
g++ -DS=2 -DSTR_SIZE=256 -DSET_SIZE=10000 -DUTYPE=double -DUNITS=nanoseconds -O3 -std=c++11 -march=native dectohex.cpp -o d2h && taskset -c 0 ./d2h