如何解决是否有一种算法可以快速将大量十六进制字符串转换为字节流?汇编/C/C++
这是我当前的代码:
//Input:hex string,1234ABCDEEFF0505DDCC ....
//Output:BYTE stream
void HexString2Hex(/*IN*/ char* hexstring,/*OUT*/ BYTE* hexBuff)
{
for (int i = 0; i < strlen(hexstring); i += 2)
{
BYTE val = 0;
if (hexstring[i] < 'A')
val += 0x10 * (hexstring[i] - '0');
else
val += 0xA0 + 0x10 * (hexstring[i] - 'A');
if (hexstring[i+1] < 'A')
val += hexstring[i + 1] - '0';
else
val += 0xA + hexstring[i + 1] - 'A';
hexBuff[i / 2] = val;
}
}
问题是:当输入的十六进制字符串非常大(比如1000000长度)时,这个函数会花费一百秒,这对我来说是不可接受的。 (CPU: i7-8700,3.2GHz。内存:32G)
那么,有没有其他算法可以更快地完成这项工作?
谢谢各位
编辑1: 谢谢稻田的评论。我太粗心了,没有注意到 strlen(time:O(n)) 被执行了数百次。所以我原来的函数是 O(n*n) 这太可怕了。
更新代码如下:
int len=strlen(hexstring);
for (int i = 0; i < len; i += 2)
而且,对于 Emanuel P 的建议,我尝试了,但似乎不太好。 下面是我的代码
map<string,BYTE> by_map;
//init table (map here)
char *xx1 = "0123456789ABCDEF";
for (int i = 0; i < 16;i++)
{
for (int j = 0; j < 16; j++)
{
_tmp[0] = xx1[i];
_tmp[1] = xx1[j];
BYTE val = 0;
if (xx1[i] < 'A')
val += 0x10 * (xx1[i] - '0');
else
val += 0xA0 + 0x10 * (xx1[i] - 'A');
if (xx1[j] < 'A')
val += xx1[j] - '0';
else
val += 0xA + xx1[j] - 'A';
by_map.insert(map<string,BYTE>::value_type(_tmp,val));
}
}
//search map
void HexString2Hex2(char* hexstring,BYTE* hexBuff)
{
char _tmp[3] = { 0 };
for (int i = 0; i < strlen(hexstring); i += 2)
{
_tmp[0] = hexstring[i];
_tmp[1] = hexstring[i + 1];
//DWORD dw = 0;
//sscanf(_tmp,"%02X",&dw);
hexBuff[i / 2] = by_map[_tmp];
}
}
编辑2: 事实上,当我修复 strlen 错误时,我的问题就解决了。 下面是我的最终代码:
void HexString2Bytes(/*IN*/ char* hexstr,/*OUT*/ BYTE* dst)
{
static uint_fast8_t LOOKUP[256];
for (int i = 0; i < 10; i++)
{
LOOKUP['0' + i] = i;
}
for (int i = 0; i < 6; i++)
{
LOOKUP['A' + i] = 0xA + i;
}
for (size_t i = 0; hexstr[i] != '\0'; i += 2)
{
*dst = LOOKUP[hexstr[i]] << 4 |
LOOKUP[hexstr[i + 1]];
dst++;
}
}
顺便说一句,衷心感谢你们。你真棒!真正的研究人员!
解决方法
创建最高效代码的标准方法(以 RAM/ROM 为代价)是使用查找表。像这样:
static const uint_fast8_t LOOKUP [256] =
{
['0'] = 0x0,['1'] = 0x1,['2'] = 0x2,['3'] = 0x3,['4'] = 0x4,['5'] = 0x5,['6'] = 0x6,['7'] = 0x7,['8'] = 0x8,['9'] = 0x9,['A'] = 0xA,['B'] = 0xB,['C'] = 0xC,['D'] = 0xD,['E'] = 0xE,['F'] = 0xF,};
这牺牲了 256 字节的只读内存,因此我们不必进行任何形式的算术运算。 uint_fast8_t
允许编译器选择更大的类型,前提是它认为这有助于提高性能。
完整的代码如下:
void hexstr_to_bytes (const char* restrict hexstr,uint8_t* restrict dst)
{
static const uint_fast8_t LOOKUP [256] =
{
['0'] = 0x0,};
for(size_t i=0; hexstr[i]!='\0'; i+=2)
{
*dst = LOOKUP[ hexstr[i ] ] << 4 |
LOOKUP[ hexstr[i+1] ];
dst++;
}
}
在 x86_64 (Godbolt) 上测试时,这可以归结为大约 10 条指令。除循环条件外,无分支。值得注意的是,没有任何错误检查,因此您必须确保其他地方的数据正常(并且包含偶数个半字节)。
测试代码:
#include <stdio.h>
#include <stdint.h>
void hexstr_to_bytes (const char* restrict hexstr,};
for(size_t i=0; hexstr[i]!='\0'; i+=2)
{
*dst = LOOKUP[ hexstr[i ] ] << 4 |
LOOKUP[ hexstr[i+1] ];
dst++;
}
}
int main (void)
{
const char hexstr[] = "DEADBEEFC0FFEE";
uint8_t bytes [(sizeof hexstr - 1)/2];
hexstr_to_bytes(hexstr,bytes);
for(size_t i=0; i<sizeof bytes; i++)
{
printf("%.2X ",bytes[i]);
}
}
,
当输入的十六进制字符串很大时(比如1000000长度)
实际上,对于今天的计算机来说,1 兆并没有那么长。
如果您需要能够处理更大的字符串(想想 10 千兆字节),甚至只是很多 1 兆字节的字符串,您可以使用 SSE 函数。虽然它适用于更温和的要求,但增加的复杂性可能不值得性能提升。
我使用的是 Windows,因此我正在使用 MSVC 2019.x64、启用优化和 arch:AVX2 进行构建。
#define _CRT_SECURE_NO_WARNINGS
typedef unsigned char BYTE;
#include <stdio.h>
#include <memory.h>
#include <intrin.h>
#include <immintrin.h>
#include <stdint.h>
static const uint_fast8_t LOOKUP[256] = {
0x00,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f };
void HexString2Bytes(/*IN*/ const char* hexstr,/*OUT*/ BYTE* dst)
{
for (size_t i = 0; hexstr[i] != '\0'; i += 2)
{
*dst = LOOKUP[hexstr[i]] << 4 |
LOOKUP[hexstr[i + 1]];
dst++;
}
}
void HexString2BytesSSE(const char* ptrin,char *ptrout,size_t bytes)
{
register const __m256i mmZeros = _mm256_set1_epi64x(0x3030303030303030ll);
register const __m256i mmNines = _mm256_set1_epi64x(0x0909090909090909ll);
register const __m256i mmSevens = _mm256_set1_epi64x(0x0707070707070707ll);
register const __m256i mmShuffle = _mm256_set_epi64x(-1,0x0f0d0b0907050301,-1,0x0f0d0b0907050301);
//============
const __m256i* in = (const __m256i*)ptrin;
__m128i* out = (__m128i *)ptrout;
size_t lines = bytes / 32;
for (size_t x = 0; x < lines; x++)
{
// Read 32 bytes
__m256i AllBytes = _mm256_load_si256(in);
// subtract '0' from every byte
AllBytes = _mm256_sub_epi8(AllBytes,mmZeros);
// Look for bytes that are 'A' or greater
const __m256i mask = _mm256_cmpgt_epi8(AllBytes,mmNines);
// Assign 7 to every byte greater than 'A'
const __m256i maskedvalues = _mm256_and_si256(mask,mmSevens);
// Subtract 7 from every byte greater than 'A'
AllBytes = _mm256_sub_epi8(AllBytes,maskedvalues);
// At this point,every byte in AllBytes represents a nibble,with
// the even bytes being the upper nibble.
// Make a copy and shift it left 4 bits to shift the nibble,plus
// 8 bits to align the nibbles.
__m256i UpperNibbles = _mm256_slli_epi64(AllBytes,4 + 8);
// Combine the nibbles
AllBytes = _mm256_or_si256(AllBytes,UpperNibbles);
// At this point,the odd numbered bytes in AllBytes is the output we want.
// Move the bytes to be contiguous. Note that you can only move
// bytes within their 128bit lane.
const __m256i ymm1 = _mm256_shuffle_epi8(AllBytes,mmShuffle);
// Move the bytes from the upper lane down next to the lower.
const __m256i ymm2 = _mm256_permute4x64_epi64(ymm1,8);
// Pull out the lowest 16 bytes
*out = _mm256_extracti128_si256(ymm2,0);
in++;
out++;
}
}
int main()
{
FILE* f = fopen("test.txt","rb");
fseek(f,SEEK_END);
size_t fsize = _ftelli64(f);
rewind(f);
// HexString2Bytes requires trailing null
char* InBuff = (char* )_aligned_malloc(fsize + 1,32);
size_t t = fread(InBuff,1,fsize,f);
fclose(f);
InBuff[fsize] = 0;
char* OutBuff = (char*)malloc(fsize / 2);
char* OutBuff2 = nullptr;
putchar('A');
for (int x = 0; x < 16; x++)
{
HexString2BytesSSE(InBuff,OutBuff,fsize);
#if 0
if (OutBuff2 == nullptr)
{
OutBuff2 = (char*)malloc(fsize / 2);
}
HexString2Bytes(InBuff,(BYTE*)OutBuff2);
if (memcmp(OutBuff,OutBuff2,fsize / 32) != 0)
printf("oops\n");
putchar('.');
#endif
}
putchar('B');
if (OutBuff2 != nullptr)
free(OutBuff2);
free(OutBuff);
_aligned_free(InBuff);
}
需要注意的几点:
- 这里没有错误处理。我不检查内存不足或文件读取错误。我什至不检查输入流中的无效字符或小写十六进制数字。
- 此代码假定字符串的大小是可用的, 不必遍历字符串(在本例中为 ftelli64)。如果您需要逐字节遍历字符串以获取其长度(a la strlen),那么您可能已经失去了这里的好处。
- 我保留了 HexString2Bytes,因此您可以比较我的代码与您的代码的输出,以确保我正确转换。
- HexString2BytesSSE 假设字符串中的字节数可以被 32 整除(一个有问题的假设)。但是,将其修改为对最后(最多)31 个字节调用 HexString2Bytes 非常简单,并且不会对性能产生太大影响。
- 我的 test.txt 有 2 场演出,这段代码运行了 16 次。这就是让差异变得显而易见的必要条件。
对于想要 kibitz 的人(因为你当然这样做),这是最内层循环的汇编输出以及一些注释:
10F0 lea rax,[rax+10h] ; Output pointer
10F4 vmovdqu ymm0,ymmword ptr [rcx] ; Input data
10F8 lea rcx,[rcx+20h]
; Convert characters to nibbles
10FC vpsubb ymm2,ymm0,ymm4 ; Subtract 0x30 from all characters
1100 vpcmpgtb ymm1,ymm2,ymm5 ; Find all characters 'A' and greater
1104 vpand ymm0,ymm1,ymm6 ; Prepare to subtract 7 from all the 'A'
1108 vpsubb ymm2,ymm0 ; Adjust all the 'A'
; Combine the nibbles to form bytes
110C vpsllq ymm1,0Ch ; Shift nibble up + align nibbles
1111 vpor ymm0,ymm2 ; Combine lower and upper nibbles
; Coalesce the odd numbered bytes
1115 vpshufb ymm2,ymm7
; Since vpshufb can't cross lanes,use vpermq to
; put all 16 bytes together
111A vpermq ymm3,8
1120 vmovdqu xmmword ptr [rax-10h],xmm3
1125 sub rdx,1
1129 jne main+0F0h (10F0h)
虽然您的最终代码几乎肯定足以满足您的需求,但我认为这对您(或未来的 SO 用户)可能会很有趣。
,也许一个开关(稍微)更快
switch (hexchar) {
default: /* error */; break;
case '0': nibble = 0; break;
case '1': nibble = 1; break;
//...
case 'F': case 'f': nibble = 15; break;
}
,
Boost 已经有 unhex
算法 implementation,您可以将基准测试结果作为基线进行比较:
unhex ( "616263646566",out ) --> "abcdef"
unhex ( "3332",out ) --> "32"
如果您的字符串非常大,那么您可以考虑一些并行方法(使用基于线程的框架,如 OpenMP、并行 STL)
,直接回答:我不知道完美的算法。 x86 asm:根据英特尔性能指南 - 展开循环。试试 XLAT 指令(需要 2 个不同的表)[消除条件分支]。修改调用接口以包含显式块长度,因为调用者应该知道字符串长度 [eliminate strlen()
]。测试输出数组空间是否足够大:小错误 - 记住奇数长度除以 2 会向下舍入。因此,如果源的长度为奇数,则初始化输出的最后一个字节(仅)。将返回值从 void 更改为 int 类型,以便您可以传递错误或成功代码和处理的长度。处理空长度输入。以块为单位的优点是实际限制成为操作系统文件大小限制。尝试设置线程关联。我怀疑性能的限制最终是 RAM 到 CPU 总线,具体取决于。如果是这样,请尝试在 RAM 支持的最大位宽上进行数据获取和存储。如果用 c 或 c++ 编码,则没有优化和更高级别的基准测试。通过执行反向过程,然后进行字节比较(非零机会 CRC-32 未命中)来测试有效性。 PBYTE 的可能问题 - 使用本机 c 无符号字符类型。在代码大小和 L1 之间有一个需要测试的权衡——缓存未命中数与循环展开的数量。在 asm 中使用 cx/ecx/rcx 进行倒计时(而不是通常的向上计数和比较)。假设 CPU 支持,SIMD 也是可能的。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。