6

下面是我当前的 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 << 4 - 但是堆分配的缓冲区似乎减慢了速度?- 结果~11ms

Antti Sykäri:用

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

结果~8ms

罗伯特_hex2asciiU_value用一个完整的 256 条目表替换,牺牲内存空间但结果 ~7ms!

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

4

16 回答 16

9

这个汇编函数(基于我之前的帖子,但我必须稍微修改一下概念才能让它真正起作用)在 Core 2 Conroe 3Ghz 的一个核心上每秒处理 33 亿个输入字符(66 亿个输出字符)。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 并将其包含在本机运行它。

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

于 2008-09-17T01:20:38.447 回答
4

以更多内存为代价,您可以创建一个完整的 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];
于 2008-09-16T03:42:49.143 回答
4

更快的 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));
}

在此处输入图像描述

于 2008-09-17T00:18:28.300 回答
3

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

于 2008-09-16T03:18:30.467 回答
3

它适用于我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"
于 2012-01-12T16:33:10.517 回答
2

对于一个,而不是乘以16做一个bitshift << 4

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

于 2008-09-16T03:17:24.367 回答
1

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

于 2008-09-16T03:20:11.943 回答
1

这是我的版本,与 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;
}
于 2008-09-16T03:38:37.967 回答
1

改变

    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 的设备来执行此操作。

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

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

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

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

(为知道你住在哪里的暴力精神病患者编写代码的强制性参考。)

于 2008-09-16T04:04:23.927 回答
1

我假设这是 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();
于 2010-12-13T17:53:48.617 回答
0

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

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

于 2008-09-16T03:41:54.683 回答
0

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

于 2008-09-16T04:57:21.617 回答
0

即使在完全指定 _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";
}
于 2008-09-16T08:05:41.243 回答
0

如果您在这里对速度非常着迷,可以执行以下操作:

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

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

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

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

因此,在计算步骤中:

  1. ABCDEFGHIJKLMNOP(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
于 2008-09-16T08:11:49.917 回答
0

我不确定一次做更多的字节会更好......你可能会得到大量的缓存未命中并显着减慢它。

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

于 2008-09-16T08:25:35.313 回答
0

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

for( const unsigned char* pChar = _pArray; pChar != pEnd; pChar++) {
    const char* pchars = _hex2asciiU_value[*pChar];
    *pszHex++ = *pchars++;
    *pszHex++ = *pchars;
}
于 2008-09-16T13:16:36.003 回答