是否有一种算法可以快速将大量十六进制字符串转换为字节流?汇编/C/C++

如何解决是否有一种算法可以快速将大量十六进制字符串转换为字节流?汇编/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 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res