【问题标题】:What's the fastest way to convert hex to integer in C++?在 C++ 中将十六进制转换为整数的最快方法是什么?
【发布时间】:2016-03-25 18:15:12
【问题描述】:

我正在尝试尽快将十六进制 char 转换为整数。

这只是一行: int x = atoi(hex.c_str);

有更快的方法吗?

在这里,我尝试了一种更动态的方法,速度略快。

int hextoint(char number) {
    if (number == '0') {
        return 0;
    }
    if (number == '1') {
        return 1;
    }
    if (number == '2') {
        return 2;
    }
    /*
     *  3 through 8
     */
    if (number == '9') {
        return 9;
    }
    if (number == 'a') {
        return 10;
    }
    if (number == 'b') {
        return 11;
    }
    if (number == 'c') {
        return 12;
    }
    if (number == 'd') {
        return 13;
    }
    if (number == 'e') {
        return 14;
    }
    if (number == 'f') {
        return 15;
    }
    return -1;
}

【问题讨论】:

  • 基准测试怎么样?
  • 你的方法只转换一位数字而且很丑陋。我们现在有std::stoi
  • 阅读文档。
  • 做基准测试:“不要在没有首先进行时间测量的情况下就代码的“效率”做出陈述。关于性能的猜测是最不可靠的”。 - Bjarne Stroustrup,
  • 您一次又一次地比较数字,这很慢(尽管编译器可能会对其进行优化)并且丑陋。你是如何计时的?微基准测试并不容易

标签: c++ performance parsing hex converter


【解决方案1】:

比 OP 的 if-else 渲染速度更快的建议解决方案:

  • 无序映射查找表

如果您的输入字符串始终是十六进制数字,您可以将查找表定义为 unordered_map

std::unordered_map<char, int> table {
{'0', 0}, {'1', 1}, {'2', 2},
{'3', 3}, {'4', 4}, {'5', 5},
{'6', 6}, {'7', 7}, {'8', 8},
{'9', 9}, {'a', 10}, {'A', 10},
{'b', 11}, {'B', 11}, {'c', 12},
{'C', 12}, {'d', 13}, {'D', 13},
{'e', 14}, {'E', 14}, {'f', 15},
{'F', 15}, {'x', 0}, {'X', 0}};

int hextoint(char number) {
  return table[(std::size_t)number];
}
  • 查找表作为用户 constexpr 文字 (C++14)

或者,如果您想要更快的东西而不是 unordered_map,您可以将新的 C++14 工具与用户文字类型一起使用,并在编译时将您的表定义为文字类型:

struct Table {
  long long tab[128];
  constexpr Table() : tab {} {
    tab['1'] = 1;
    tab['2'] = 2;
    tab['3'] = 3;
    tab['4'] = 4;
    tab['5'] = 5;
    tab['6'] = 6;
    tab['7'] = 7;
    tab['8'] = 8;
    tab['9'] = 9;
    tab['a'] = 10;
    tab['A'] = 10;
    tab['b'] = 11;
    tab['B'] = 11;
    tab['c'] = 12;
    tab['C'] = 12;
    tab['d'] = 13;
    tab['D'] = 13;
    tab['e'] = 14;
    tab['E'] = 14;
    tab['f'] = 15;
    tab['F'] = 15;
  }
  constexpr long long operator[](char const idx) const { return tab[(std::size_t) idx]; } 
} constexpr table;

constexpr int hextoint(char number) {
  return table[(std::size_t)number];
}

Live Demo

基准测试:

我使用 Nikos Athanasiou 编写的代码运行基准测试,该代码最近发布在 isocpp.org 上,作为 C++ 微基准测试的建议方法。

比较的算法有:

1.欧派原创if-else

long long hextoint3(char number) {
  if(number == '0') return 0;
  if(number == '1') return 1;
  if(number == '2') return 2;
  if(number == '3') return 3;
  if(number == '4') return 4;
  if(number == '5') return 5;
  if(number == '6') return 6;
  if(number == '7') return 7;
  if(number == '8') return 8;
  if(number == '9') return 9;
  if(number == 'a' || number == 'A') return 10;
  if(number == 'b' || number == 'B') return 11;
  if(number == 'c' || number == 'C') return 12;
  if(number == 'd' || number == 'D') return 13;
  if(number == 'e' || number == 'E') return 14;
  if(number == 'f' || number == 'F') return 15;
  return 0;
}

2。紧凑的 if-else,由 Christophe 提出:

long long hextoint(char number) {
  if (number >= '0' && number <= '9') return number - '0';
  else if (number >= 'a' && number <= 'f') return number - 'a' + 0x0a;
  else if (number >= 'A' && number <= 'F') return number - 'A' + 0X0a;
  else return 0;
}

3.更正了 g24l 提出的同时处理大写字母输入的三元运算符版本:

long long hextoint(char in) {
  int const x = in;
  return (x <= 57)? x - 48 : (x <= 70)? (x - 65) + 0x0a : (x - 97) + 0x0a;
}

4.查找表 (unordered_map):

long long hextoint(char number) {
  return table[(std::size_t)number];
}

其中table 是前面显示的无序映射。

5.查找表(用户constexpr 文字):

long long hextoint(char number) {
  return table[(std::size_t)number];
}

其中 table 是用户定义的文字,如上所示。

实验设置

我定义了一个将输入的十六进制字符串转换为整数的函数:

long long hexstrtoint(std::string const &str, long long(*f)(char)) {
  long long ret = 0;
  for(int j(1), i(str.size() - 1); i >= 0; --i, j *= 16) {
    ret += (j * f(str[i]));
  }
  return ret;
}

我还定义了一个用随机十六进制字符串填充字符串向量的函数:

std::vector<std::string>
populate_vec(int const N) {
  random_device rd;
  mt19937 eng{ rd() };
  uniform_int_distribution<long long> distr(0, std::numeric_limits<long long>::max() - 1);
  std::vector<std::string> out(N);
  for(int i(0); i < N; ++i) {
    out[i] = int_to_hex(distr(eng));
  }
  return out;
}

我创建了分别填充有 50000、100000、150000、200000 和 250000 个随机十六进制字符串的向量。然后对于每个算法,我运行 100 次实验并平均时间结果。

编译器是 GCC 版本 5.2,带有优化选项 -O3

结果:

讨论

从结果我们可以得出结论,对于这些实验设置,建议的表格方法优于所有其他方法。 if-else 方法是迄今为止最差的,因为 unordered_map 尽管它赢得了 if-else 方法,但它比其他提议的方法慢得多。

CODE

编辑:

stgatilov 提出的方法的结果,按位运算:

long long hextoint(char x) {
    int b = uint8_t(x);
    int maskLetter = (('9' - b) >> 31);
    int maskSmall = (('Z' - b) >> 31);
    int offset = '0' + (maskLetter & int('A' - '0' - 10)) + (maskSmall & int('a' - 'A'));
    return b - offset;
}

编辑:

我还针对表格方法测试了来自 g24l 的原始代码:

long long hextoint(char in) {
  long long const x = in;
  return x < 58? x - 48 : x - 87;
}

请注意,此方法不处理大写字母 ABCDEF

结果:

表格方法仍然渲染得更快。

【讨论】:

  • @101010:没有解释为什么你认为这是最快,或者在什么情况下你认为它是最快的。示例:如果它很少被调用,并且您必须等待内存,那么它将比任何基于逻辑+数学的解决方案都要慢。
  • @KarolyHorvath 我猜你没有阅读 OP 的评论“文字类型表最适合我”
  • @101010:是的,谢谢,我错过了。我是否应该相信他的评估是另一回事......
  • 即使针对这样简单的问题提出unordered_map 也是矫枉过正(除非您想测试您的 unordered_map 实现)。为什么不简单地使用char tab[256] = { -1, -1, ... 0, 1, 2, 3, 4, ... -1, -1, ... 10, 11, 12...}。此外,提出的示例解决方案并不等效:有些不会处理意外数据,有些会。 arm(手机)上的基准测试结果很可能也不同。
  • 请注意,ISO C至少(不确定C++)要求'0'...'9'是连续的,但不是'A'...'Z',所以方法#2 是非标准的,即使它适用于大多数系统。方法 #3 更糟糕,因为它使用数字 ASCII 代码。
【解决方案2】:

这个问题显然在不同的系统上有不同的答案,从这个意义上说,它从一开始就是病态的。例如,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 CEVbitwise BEVlookup table LUT 进行了一组测试随机生成的字符串。测试相当简单,来自:

Test source code

这些是可验证的:

  1. 输入字符串的本地副本每次都放置在本地缓冲区中。
  2. 保留结果的本地副本,然后将其复制到每个字符串测试的堆中
  3. 持续时间仅用于提取对字符串进行操作的时间
  4. 统一方法,没有适合其他情况的复杂机制和环绕/环绕代码。
  5. 无采样使用整个时序
  6. CPU 预热已执行
  7. 睡眠在测试之间发生以允许编组代码,这样一个测试就不会利用前一个测试。
  8. 编译使用g++ -std=c++11 -O3 -march=native dectohex.cpp -o d2h进行
  9. 启动taskset -c 0 d2h
  10. 没有线程依赖或多线程
  11. 实际使用结果,以避免任何类型的循环优化

作为旁注,我在实践中看到版本 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

【讨论】:

  • 我想知道为什么在第一种情况下结果是 2280 us,而在第二种情况下结果是 2050 us,尽管代码完全相同。恐怕您的基准测试方法存在一些问题。不幸的是,我无法在本地对其进行测试(MinGW 没有高分辨率计时器)。如果我将每个基准测试部分放入另一个循环中进行 1000 次迭代,我会在具有 GCC 4.8.3 和相同编译器键的 Ivy Bridge 机器上获得值 539031 和 1314075。
  • @stgatilov ,您可以自己测试并决定。这就是答案在这里所说的。代码可用,你可以通过在不同的可引导分区上安装 linux 来获得一个非常好的 g++ 编译器。
  • @stgatilov 不同之处可能在于处理器调度。
  • 您的 BEV 是否正确,还是应该是 9 * (x &gt;&gt; 6) + (x &amp; 0xf)?您的 pastebin 还使用 decfromhex2 的三元组。
  • 你可能想要unsigned char n,所以它索引 0..255 范围,而不是 -128 到 +127。
【解决方案3】:

嗯,这是一个奇怪的问题。将单个十六进制字符转换为整数是如此之快,以至于很难判断哪个更快,因为所有方法几乎都可能比您编写的代码更快,以便使用它们 =)

我会假设以下几点:

  1. 我们有一个现代 x86(64) CPU。
  2. 输入字符的 ASCII 码存储在通用寄存器中,例如在eax
  3. 必须在通用寄存器中获得输出整数。
  4. 保证输入字符是有效的十六进制数字(16 种情况之一)。

解决方案

现在这里有几种解决问题的方法:第一种基于查找,两种基于三元运算符,最后一种基于位运算:

int hextoint_lut(char x) {
    static char lut[256] = {???};
    return lut[uint8_t(x)];
}

int hextoint_cond(char x) {
    uint32_t dig = x - '0';
    uint32_t alp = dig + ('0' - 'a' + 10);
    return dig <= 9U ? dig : alp;
}
int hextoint_cond2(char x) {
    uint32_t offset = (uint8_t(x) <= uint8_t('9') ? '0' : 'a' - 10);
    return uint8_t(x) - offset;
}

int hextoint_bit(char x) {
    int b = uint8_t(x);
    int mask = (('9' - b) >> 31);
    int offset = '0' + (mask & int('a' - '0' - 10));
    return b - offset;
}

以下是生成的相应装配清单(仅显示相关部分):

;hextoint_lut;
movsx   eax, BYTE PTR [rax+rcx]   ; just load the byte =)

;hextoint_cond;
sub edx, 48                       ; subtract '0'
cmp edx, 9                        ; compare to '9'
lea eax, DWORD PTR [rdx-39]       ; add ('0' - 'a' + 10)
cmovbe  eax, edx                  ; choose between two cases in branchless way

;hextoint_cond2;                  ; (modified slightly)
mov eax, 48                       
mov edx, 87                       ; set two offsets to registers
cmp ecx, 57                       ; compare with '9'
cmovbe  edx, eax                  ; choose one offset
sub ecx, edx                      ; subtract the offset

;hextoint_bit;
mov ecx, 57                       ; load '9'
sub ecx, eax                      ; get '9' - x
sar ecx, 31                       ; convert to mask if negative
and ecx, 39                       ; set to 39 (for x > '9')
sub eax, ecx                      ; subtract 39 or 0
sub eax, 48                       ; subtract '0'

分析

我将尝试从吞吐量意义上估计每种方法所花费的周期数,这实质上是一次处理大量数字时每个输入数字所花费的时间。以 Sandy Bridge 架构为例。

hextoint_lut 函数由单个内存加载组成,它在端口 2 或 3 上占用 1 uop。这两个端口都专用于内存加载,并且它们内部还具有地址计算功能,可以进行 @987654327 @无需额外费用。有两个这样的端口,每个端口可以在一个周期内执行一个微指令。所以据说这个版本需要 0.5 个时钟时间。如果我们必须从内存中加载输入数字,则每个值需要多一次内存加载,因此总成本将是 1 个时钟。

hextoint_cond 版本有 4 条指令,但 cmov 被分成两个独立的微指令。所以总共有 5 个微指令,每个微指令都可以在三个算术端口 0、1 和 5 中的任何一个上处理。所以据说需要 5/3 个周期时间。请注意,内存加载端口是空闲的,因此即使您必须从内存中加载输入值,时间也不会增加。

hextoint_cond2 版本有 5 条指令。但是在一个紧密的循环中,可以将常量预加载到寄存器中,因此只有比较、cmov 和减法。它们总共是 4 个微指令,每个值提供 4/3 个周期(即使有内存读取)。

hextoint_bit 版本是保证没有分支和查找的解决方案,如果您不想总是检查编译器是否生成了 cmov 指令,这很方便。第一个 mov 是免费的,因为可以在紧密循环中预加载常量。其余的是 5 条算术指令,在端口 0、1、5 中有 5 条微指令。因此它应该需要 5/3 个周期(即使读取内存)。

基准测试

我已经对上述 C++ 函数进行了基准测试。在基准测试中,会生成 64 KB 的随机数据,然后每个函数在该数据上运行多次。所有结果都添加到校验和中,以确保编译器不会删除代码。使用手动 8 倍展开。我在 Ivy Bridge 3.4 Ghz 核心上进行了测试,它与 Sandy Bridge 非常相似。每个输出字符串包含:函数名称、基准测试花费的总时间、每个输入值的周期数、所有输出的总和。

Benchmark code

MSVC2013 x64 /O2:
hextoint_lut: 0.741 sec, 1.2 cycles  (check: -1022918656)
hextoint_cond: 1.925 sec, 3.0 cycles  (check: -1022918656)
hextoint_cond2: 1.660 sec, 2.6 cycles  (check: -1022918656)
hextoint_bit: 1.400 sec, 2.2 cycles  (check: -1022918656)

GCC 4.8.3 x64 -O3 -fno-tree-vectorize
hextoint_lut: 0.702 sec, 1.1 cycles  (check: -1114112000)
hextoint_cond: 1.513 sec, 2.4 cycles  (check: -1114112000)
hextoint_cond2: 2.543 sec, 4.0 cycles  (check: -1114112000)
hextoint_bit: 1.544 sec, 2.4 cycles  (check: -1114112000)

GCC 4.8.3 x64 -O3
hextoint_lut: 0.702 sec, 1.1 cycles  (check: -1114112000)
hextoint_cond: 0.717 sec, 1.1 cycles  (check: -1114112000)
hextoint_cond2: 0.468 sec, 0.7 cycles  (check: -1114112000)
hextoint_bit: 0.577 sec, 0.9 cycles  (check: -1114112000)

显然,LUT 方法每个值需要一个周期(如预测的那样)。其他方法通常每个值需要 2.2 到 2.6 个周期。在 GCC 的情况下,hextoint_cond2 很慢,因为编译器使用 cmp+sbb+and 魔法而不是所需的 cmov 指令。另请注意,默认情况下,GCC 对大多数方法(最后一段)进行矢量化,这比不可矢量化的 LUT 方法提供了预期更快的结果。请注意,手动矢量化会带来更大的提升。

讨论

注意hextoint_cond 使用普通条件跳转而不是cmov 会有一个分支。假设随机输入十六进制数字,几乎总是会被错误预测。所以我认为性能会很糟糕。

我已经分析了吞吐量性能。但是如果我们必须处理大量的输入值,那么我们绝对应该对转换进行矢量化以获得更好的速度。 hextoint_cond 可以通过 SSE 以非常简单的方式矢量化。它允许仅使用 4 条指令来处理 16 字节到 16 字节,我想大约需要 2 个周期。

请注意,为了查看任何性能差异,您必须确保所有输入值都适合缓存(L1 是最好的情况)。如果您从主内存中读取输入数据,即使std::atoi 与所考虑的方法一样快 =)

此外,您应该将主循环展开 4 倍甚至 8 倍以获得最佳性能(以消除循环开销)。 您可能已经注意到,这两种方法的速度很大程度上取决于代码周围的操作。例如。添加内存负载会使第一种方法花费的时间增加一倍,但不会影响其他方法。

P.S.很可能您并不真的需要对此进行优化。

【讨论】:

  • 在您的条件版本中,您不必要地进行了一次减法。这减少到 3uop 。顺便说一句,我生成了随机字符串,仍然没有问题。您的程序集是如何生成的?
  • @g24l:我使用 MSVC2013 编译了函数。我猜你的意思是我们可以在 39 和 87 上做一个三元运算符并减去它们中的任何一个?在这种情况下,一个减法将被 mov 替换为立即数。似乎是 4 微指令(mov 是免费的),为什么是 3?
  • @g24l:我添加了一个单减法版本。还有一个有点魔力的版本。底线很明确:没有 LUT 的方法大约执行 5 微秒,这应该是大约 5/3 周期的吞吐量。
  • 你的分析确实很有趣。我会尝试写一些测试。我也考虑过 bitmagic 版本(确实是好把戏),但我认为它不会有太大的不同。正如你所说,周围的代码可能更重要。
  • 在第一个版本中,sub 之前的lea 会缩短关键路径延迟:leasubcmp 都可以并行运行以馈送cmov。此外,如果您使用 cmovbcmovae(1 微指令)而不是 cmovbe(2 微指令,Broadwell/Skylake 将受益,因为它需要读取分别重命名的 ZF 和 CF:最近的英特尔通过指令避免标志合并只阅读他们需要的 FLAGS 部分。)uops.info 有计数。无论如何,将您的 cmp 常量调整一,除非这是您无法手动修复的编译器输出。
【解决方案4】:

这是我最喜欢的十六进制转整数代码:

inline int htoi(int x) {
    return 9 * (x >> 6) + (x & 017);
}

字母不区分大小写,即返回“a”和“A”的正确结果。

【讨论】:

    【解决方案5】:

    假设您的函数是为一个有效的十六进制数字调用的,它平均至少需要 8 次比较操作(可能是 7 次跳转)。相当昂贵。

    另一种选择是更紧凑:

    if (number >= '0' && number<='9') 
        return number-'0';
    else if (number >= 'a' && number <='f') 
        return number-'a'+0x0a; 
    else return -1; 
    

    另一种选择是使用查找表(以空间换取速度),您只需初始化一次,然后直接访问:

    if (number>=0) 
       return mytable[number]; 
    else return -1;   
    

    如果你想一次转换多个数字,你可以看看this question)

    编辑:基准测试

    根据Ike 的观察,我编写了一个小型非正式基准测试(可在online 此处获得),您可以在自己喜欢的编译器上运行。

    结论:

    • 查找表永远是赢家
    • 开关优于 if 链。
    • 使用msvc2015(发布),第二好的是我的精简版,紧随其后的是101010的地图版。
    • 在 ideone 上使用 gcc,第二个是 switch 版本,然后是 compact 版本。

    【讨论】:

    • 为什么编译器会发出需要所有这些条件的代码? number 显然没有改变。我希望创建一个查找表。当然,最好写一个switch
    • @LightnessRacesinOrbit 出于好奇,我尝试了这些:(一堆 ifs:goo.gl/FJWn0Y)(开关:goo.gl/xUPJaF)(范围检查:goo.gl/gIeK2y)。此外,如果有人想在答案中使用这些(最好是使用基准测试),请随意。
    • @Ike 有趣的是,开关生成的代码等效于表格方法!我预计它会生成一个跳转表,但每个条目都有一个 movl 和一个 ret。
    • @Christophe 最让我吃惊的是它把它变成了一个紧凑的 LUT。我猜它是其中性能最好的一个,尽管我对计算机架构的了解不够,无法完全自信地预测,而无需至少根据仔细的基准对其进行分析。
    • 程序员每次假设ASCII编码,上帝就杀了一只小猫。 ;-)
    【解决方案6】:

    如果您(或其他人)实际转换一组值,我制作了一个 AVX2 SIMD 编码器和解码器,其基准测试速度比最快的标量实现快约 12 倍:https://github.com/zbjornson/fast-hex

    16 个十六进制值可以方便地(两次)放入 YMM 寄存器,因此您可以使用 PSHUFB 进行并行查找。解码有点困难,并且基于位操作。

    【讨论】:

      猜你喜欢
      • 2013-12-31
      • 2010-10-16
      • 2011-08-11
      • 1970-01-01
      • 2014-04-16
      • 1970-01-01
      • 2017-08-12
      相关资源
      最近更新 更多