下面是我当前的 char* 到十六进制字符串函数。我把它写成位操作的练习。在 AMD Athlon MP 2800+ 上,十六进制化 1000 万字节数组大约需要 7 毫秒。我是否缺少任何技巧或其他方法?

我怎样才能让它更快?

在 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;
}

更新

添加了计时代码

布莱恩·R。邦迪:用堆分配的缓冲区替换 std::string 并将 ofs*16 更改为 ofs << 4 - 但是堆分配的缓冲区似乎会减慢速度?- 结果~11ms

安蒂·西卡里: 将内循环替换为

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

结果~8ms

罗伯特:代替 _hex2asciiU_value 具有完整的 256 个条目的表,牺牲了内存空间,但结果约为 7 毫秒!

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

有帮助吗?

解决方案

您可以以更多内存为代价创建完整的 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];

其他提示

这个汇编函数(基于我之前的文章,但我必须稍微修改一下概念才能使其实际工作)在 Core 2 Conroe 3Ghz 的一个内核上每秒处理 33 亿个输入字符(66 亿个输出字符)。彭林可能更快。

%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,我在这么琐碎的事情上浪费时间有错吗?或者这很棒吗?

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

enter image description here

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

它对我有用 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"

其一,不是乘以 16 做一个 bitshift << 4

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

不会有太大区别...*pChar-(ofs*16) 可以用 [*pCHAR & 0x0F] 完成

这是我的版本,与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;
}

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

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();

改变

    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% 的加速。

按照建议一次写入两个字节的结果 罗伯特 导致大约 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];
}

一次执行 2 个字节或一次执行 4 个字节可能会带来更大的加速,正如所指出的 艾伦·温德, ,但是当您必须处理奇怪的字符时,事情就会变得更加棘手。

如果你喜欢冒险,你可以尝试适应 达夫的装置 去做这个。

结果是在 Intel Core Duo 2 处理器上进行的 gcc -O3.

始终测量 你实际上会得到更快的结果——假装是优化的悲观主义并非毫无价值。

总是测试 你会得到正确的结果——假装是优化的错误是非常危险的。

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

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

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

您知道,gcc 中的“-O1”到“-03”等标志。

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

如果您对速度相当着迷,您可以执行以下操作:

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

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

  1. 使用乘法或类似指令将四位值解压缩为 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

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

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

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

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

即使完全指定了 _hex2asciiU_value ,当我编写本文时显示的函数也会产生不正确的输出。以下代码有效,在我的 2.33GHz Macbook Pro 上运行 200,000,0 亿个字符大约需要 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";
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top