【问题标题】:char[] to hex string exercisechar[] 转十六进制字符串练习
【发布时间】:2010-09-09 07:21:34
【问题描述】:

下面是我当前的 char* 到十六进制字符串函数。我把它写成一个位操作的练习。 AMD Athlon MP 2800+ 需要大约 7 毫秒来对 1000 万字节数组进行 hexify。有什么我错过的技巧或其他方式吗?

我怎样才能加快速度?

在 g++ 中使用 -O3 编译

static const char _hex2asciiU_value[256][2] =
     { {'0','0'}, {'0','1'}, /* snip..., */ {'F','E'},{'F','F'} };

std::string char_to_hex( const unsigned char* _pArray, unsigned int _len )
{
    std::string str;
    str.resize(_len*2);
    char* pszHex = &str[0];
    const unsigned char* pEnd = _pArray + _len;

    clock_t stick, etick;
    stick = clock();
    for( const unsigned char* pChar = _pArray; pChar != pEnd; pChar++, pszHex += 2 ) {
        pszHex[0] = _hex2asciiU_value[*pChar][0];
        pszHex[1] = _hex2asciiU_value[*pChar][1];
    }
    etick = clock();

    std::cout << "ticks to hexify " << etick - stick << std::endl;

    return str;
}

更新

添加计时码

Brian R. Bondy: 用堆分配的缓冲区替换 std::string 并将 ofs*16 更改为 ofs

Antti Sykäri:用

替换内循环
 int upper = *pChar >> 4;
 int lower = *pChar & 0x0f;
 pszHex[0] = pHex[upper];
 pszHex[1] = pHex[lower];

结果 ~8ms

Robert:将_hex2asciiU_value 替换为完整的 256 项表,牺牲内存空间但结果约为 7ms!

HoyHoy: 注意到它产生了不正确的结果

【问题讨论】:

  • 所写的功能似乎不再起作用
  • 您可以使用我添加的 for 循环初始化您的 _hex2asciiU_value 数组。另外,我注意到我的答案有一个缺陷,我将提到的数组的长度指定为 255 而不是 256。

标签: c++ optimization hex


【解决方案1】:

这个汇编函数(基于我之前的帖子,但我必须稍微修改一下概念才能让它真正起作用)在 Core 2 的一个核心上每秒处理 33 亿个输入字符(66 亿个输出字符)康罗 3Ghz。 Penryn 可能更快。

%include "x86inc.asm"

SECTION_RODATA
pb_f0: times 16 db 0xf0
pb_0f: times 16 db 0x0f
pb_hex: db 48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70

SECTION .text

; int convert_string_to_hex( char *input, char *output, int len )

cglobal _convert_string_to_hex,3,3
    movdqa xmm6, [pb_f0 GLOBAL]
    movdqa xmm7, [pb_0f GLOBAL]
.loop:
    movdqa xmm5, [pb_hex GLOBAL]
    movdqa xmm4, [pb_hex GLOBAL]
    movq   xmm0, [r0+r2-8]
    movq   xmm2, [r0+r2-16]
    movq   xmm1, xmm0
    movq   xmm3, xmm2
    pand   xmm0, xmm6 ;high bits
    pand   xmm2, xmm6
    psrlq  xmm0, 4
    psrlq  xmm2, 4
    pand   xmm1, xmm7 ;low bits
    pand   xmm3, xmm7
    punpcklbw xmm0, xmm1
    punpcklbw xmm2, xmm3
    pshufb xmm4, xmm0
    pshufb xmm5, xmm2
    movdqa [r1+r2*2-16], xmm4
    movdqa [r1+r2*2-32], xmm5
    sub r2, 16
    jg .loop
    REP_RET

请注意,它使用 x264 汇编语法,这使得它更易于移植(到 32 位和 64 位等)。将其转换为您选择的语法很简单:r0、r1、r2 是寄存器中函数的三个参数。它有点像伪代码。或者您可以从 x264 树中获取 common/x86/x86inc.asm 并将其包含在本机运行它。

附: Stack Overflow,我在这样一件微不足道的事情上浪费时间是错的吗?或者这很棒吗?

【讨论】:

  • 哇。从来不知道 x264!
【解决方案2】:

以更多内存为代价,您可以创建一个完整的 256 项十六进制代码表:

static const char _hex2asciiU_value[256][2] =
    { {'0','0'}, {'0','1'}, /* ..., */ {'F','E'},{'F','F'} };

然后直接索引到表中,不需要一点点摆弄。

const char *pHexVal = pHex[*pChar];
pszHex[0] = pHexVal[0];
pszHex[1] = pHexVal[1];

【讨论】:

    【解决方案3】:

    更快的 C 实现

    它的运行速度比 C++ 实现快近 3 倍。不知道为什么,因为它非常相似。对于我发布的最后一个 C++ 实现,运行 200,000,000 个字符数组需要 6.8 秒。实施只用了 2.2 秒。

    #include <stdio.h>
    #include <stdlib.h>
    
    char* char_to_hex(const unsigned char* p_array, 
                      unsigned int p_array_len,
                      char** hex2ascii)
    {
        unsigned char* str = malloc(p_array_len*2+1);
        const unsigned char* p_end = p_array + p_array_len;
        size_t pos=0;
        const unsigned char* p;
        for( p = p_array; p != p_end; p++, pos+=2 ) {
           str[pos] = hex2ascii[*p][0];
           str[pos+1] = hex2ascii[*p][1];
        }
        return (char*)str;
    }
    
    int main()
    {
      size_t hex2ascii_len = 256;
      char** hex2ascii;
      int i;
      hex2ascii = malloc(hex2ascii_len*sizeof(char*));
      for(i=0; i<hex2ascii_len; i++) {
        hex2ascii[i] = malloc(3*sizeof(char));    
        snprintf(hex2ascii[i], 3,"%02X", i);
      }
      size_t len = 8;
      const unsigned char a[] = "DO NOT WANT";
      printf("%s\n", char_to_hex((const unsigned char*)a, len, (char**)hex2ascii));
    }
    

    【讨论】:

    • 你用什么来绘制这张图?
    • 你为终止的 NUL 分配了空间,但从未分配它。
    【解决方案4】:

    一次操作 32 位(4 个字符),然后在需要时处理尾部。当我使用 url 编码进行此练习时,每个字符的全表查找比逻辑构造稍快,因此您可能还需要在上下文中进行测试以考虑缓存问题。

    【讨论】:

      【解决方案5】:

      unsigned char 对我有用:

      unsigned char  c1 =  byteVal >> 4;
      unsigned char  c2 =  byteVal & 0x0f;
      
      c1 +=  c1 <= 9 ? '0' : ('a' - 10);
      c2 +=  c2 <= 9 ? '0' : ('a' - 10);
      
      std::string sHex("  ");
      sHex[0] = c1 ;
      sHex[1] = c2 ;
      
      
      //sHex - contain what we need. For example "0f"
      

      【讨论】:

        【解决方案6】:

        对于一个,而不是乘以 16 做一个 bitshift &lt;&lt; 4

        也不要使用std::string,而是在堆上创建一个缓冲区,然后delete 它。这将比从字符串中需要的对象销毁更有效。

        【讨论】:

        • 编译器应该为你做这件事。位移而不是乘法使代码的可读性大大降低。
        • 这并不是说他的代码没有其他位移......而且不能保证他的编译器会这样做。他的具体问题是如何提高效率。
        • 也不能保证乘法比移位快。
        【解决方案7】:

        不会有很大的不同... *pChar-(ofs*16) 可以用 [*pCHAR & 0x0F] 完成

        【讨论】:

          【解决方案8】:

          这是我的版本,与 OP 的版本不同,它不假定 std::basic_string 的数据位于连续区域中:

          #include <string>
          
          using std::string;
          
          static char const* digits("0123456789ABCDEF");
          
          string
          tohex(string const& data)
          {
              string result(data.size() * 2, 0);
              string::iterator ptr(result.begin());
              for (string::const_iterator cur(data.begin()), end(data.end()); cur != end; ++cur) {
                  unsigned char c(*cur);
                  *ptr++ = digits[c >> 4];
                  *ptr++ = digits[c & 15];
              }
              return result;
          }
          

          【讨论】:

            【解决方案9】:

            我假设这是 Windows+IA32。
            尝试使用 short int 而不是两个十六进制字母。

            short int hex_table[256] = {'0'*256+'0', '1'*256+'0', '2'*256+'0', ..., 'E'*256+'F', 'F'*256+'F'};
            unsigned short int* pszHex = &str[0];
            
            stick = clock();
            
            for (const unsigned char* pChar = _pArray; pChar != pEnd; pChar++) 
                *pszHex++ = hex_table[*pChar];
            
            etick = clock();
            

            【讨论】:

              【解决方案10】:

              变化

                  ofs = *pChar >> 4;
                  pszHex[0] = pHex[ofs];
                  pszHex[1] = pHex[*pChar-(ofs*16)];
              

                  int upper = *pChar >> 4;
                  int lower = *pChar & 0x0f;
                  pszHex[0] = pHex[upper];
                  pszHex[1] = pHex[lower];
              

              导致大约 5% 的加速。

              按照Robert 的建议一次写入两个字节的结果可提高约 18% 的速度。代码更改为:

              _result.resize(_len*2);
              short* pszHex = (short*) &_result[0];
              const unsigned char* pEnd = _pArray + _len;
              
              const char* pHex = _hex2asciiU_value;
              for(const unsigned char* pChar = _pArray;
                  pChar != pEnd;
                  pChar++, ++pszHex )
              {
                  *pszHex = bytes_to_chars[*pChar];
              }
              

              需要初始化:

              short short_table[256];
              
              for (int i = 0; i < 256; ++i)
              {
                  char* pc = (char*) &short_table[i];
                  pc[0] = _hex2asciiU_value[i >> 4];
                  pc[1] = _hex2asciiU_value[i & 0x0f];
              }
              

              正如Allan Wind 所指出的那样,一次处理 2 个字节或一次处理 4 个字节可能会带来更大的加速,但是当您必须处理奇数字符时,它会变得更加棘手。

              如果您喜欢冒险,可以尝试调整 Duff's device 来执行此操作。

              结果基于 Intel Core Duo 2 处理器和gcc -O3

              始终衡量您实际上获得了更快的结果 - 伪装成优化的悲观化并非毫无价值。

              总是测试你得到正确的结果 - 一个伪装成优化的错误是非常危险的。

              并且始终牢记速度和可读性之间的权衡 - 生命太短,任何人都无法维护不可读的代码。

              Obligatory reference 编码为violent psychopath who knows where you live。)

              【讨论】:

                【解决方案11】:

                确保您的编译器优化已开启至最高工作级别。

                你知道,像 gcc 中的 '-O1' 到 '-03' 这样的标志。

                【讨论】:

                  【解决方案12】:

                  我发现在数组中使用索引而不是指针可以加快速度。这完全取决于您的编译器选择如何优化。关键是处理器有指令可以在单个指令中执行复杂的事情,例如 [i*2+1]。

                  【讨论】:

                  • 似乎很可疑!数组的索引只是指针算术!
                  【解决方案13】:

                  如果您对这里的速度比较着迷,可以执行以下操作:

                  每个字符是一个字节,代表两个十六进制值。因此,每个字符实际上是两个四位值。

                  因此,您可以执行以下操作:

                  1. 使用乘法或类似指令将 4 位值解压缩为 8 位值。
                  2. 使用 pshufb,即 SSSE3 指令(但仅限 Core2)。它采用 16 个 8 位输入值的数组,并根据第二个向量中的 16 个 8 位索引对它们进行混洗。由于您只有 16 个可能的字符,因此非常适合;输入数组是 0 到 F 个字符的向量,索引数组是解压缩的 4 位值数组。

                  因此,在单条指令中,您将执行 16 次表查找,所用时钟少于通常只执行一次所需的时钟(pshufb 在 Penryn 上是 1 个时钟延迟)。

                  所以,在计算步骤中:

                  1. A B C D E F G H I J K L M N O P(64 位输入值向量,“向量 A”)-> 0A 0B 0C 0D 0E 0F 0G 0H 0I 0J 0K 0L 0M 0N 0O 0P(128 位索引向量,“向量 B”)。最简单的方法可能是两个 64 位乘法。
                  2. pshub [0123456789ABCDEF],向量 B

                  【讨论】:

                    【解决方案14】:

                    我不确定一次处理更多字节是否会更好...您可能会遇到大量缓存未命中并显着减慢速度。

                    您可能会尝试展开循环,采取更大的步骤并在每次循环中执行更多字符,以消除一些循环开销。

                    【讨论】:

                    • 一次更多字节应该可以很好地工作,取决于系统的字大小
                    【解决方案15】:

                    在我的 Athlon 64 4200+ 上始终保持约 4 毫秒(原始代码约 7 毫秒)

                    for( const unsigned char* pChar = _pArray; pChar != pEnd; pChar++) {
                        const char* pchars = _hex2asciiU_value[*pChar];
                        *pszHex++ = *pchars++;
                        *pszHex++ = *pchars;
                    }
                    

                    【讨论】:

                      【解决方案16】:

                      即使完全指定了 _hex2asciiU_value,我在写这篇文章时显示的函数也会产生不正确的输出。以下代码有效,在我的 2.33GHz Macbook Pro 上运行 200,000,000 百万个字符大约需要 1.9 秒。

                      #include <iostream>
                      
                      using namespace std;
                      
                      static const size_t _h2alen = 256;
                      static char _hex2asciiU_value[_h2alen][3];
                      
                      string char_to_hex( const unsigned char* _pArray, unsigned int _len )
                      {
                          string str;
                          str.resize(_len*2);
                          char* pszHex = &str[0];
                          const unsigned char* pEnd = _pArray + _len;
                          const char* pHex = _hex2asciiU_value[0];
                          for( const unsigned char* pChar = _pArray; pChar != pEnd; pChar++, pszHex += 2 ) {
                             pszHex[0] = _hex2asciiU_value[*pChar][0];
                             pszHex[1] = _hex2asciiU_value[*pChar][1];
                          }
                          return str;
                      }
                      
                      
                      int main() {
                        for(int i=0; i<_h2alen; i++) {
                          snprintf(_hex2asciiU_value[i], 3,"%02X", i);
                        }
                        size_t len = 200000000;
                        char* a = new char[len];
                        string t1;
                        string t2;
                        clock_t start;
                        srand(time(NULL));
                        for(int i=0; i<len; i++) a[i] = rand()&0xFF;
                        start = clock();
                        t1=char_to_hex((const unsigned char*)a, len);
                        cout << "char_to_hex conversion took ---> " << (clock() - start)/(double)CLOCKS_PER_SEC << " seconds\n";
                      }
                      

                      【讨论】:

                      • >200,000,000 百万个字符
                      猜你喜欢
                      • 1970-01-01
                      • 2020-07-18
                      • 1970-01-01
                      • 1970-01-01
                      • 2018-01-31
                      相关资源
                      最近更新 更多