Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/c/72.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
是否有一种算法可以将大量十六进制字符串快速转换为字节流?asm/C/C++_C++_C_Assembly_Optimization_Hex - Fatal编程技术网

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

是否有一种算法可以将大量十六进制字符串快速转换为字节流?asm/C/C++,c++,c,assembly,optimization,hex,C++,C,Assembly,Optimization,Hex,这是我目前的代码: //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

这是我目前的代码:

//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长度时,此函数将花费100秒,这对我来说是不可接受的。CPU:i7-8700,3.2GHz。内存:32G

那么,有没有其他算法可以更快地完成这项工作

谢谢你们

编辑1: 谢谢帕迪的评论。我太粗心了,没有注意到斯特伦时间:On被执行了数百次。我原来的函数是在*n上,这太糟糕了

更新代码如下:

int len=strlen(hexstring);
for (int i = 0; i < len; i += 2)
而且,对于伊曼纽尔·p的建议,我试过了,似乎不太好。 下面是我的代码

map<string, BYTE> by_map;
编辑2: 事实上,当我修复strlen bug时,我的问题就解决了。 以下是我的最终代码:

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

顺便说一句,衷心感谢你们。你真棒!真正的研究人员

也许一个开关稍微快一点

switch (hexchar) {
    default: /* error */; break;
    case '0': nibble = 0; break;
    case '1': nibble = 1; break;
    //...
    case 'F': case 'f': nibble = 15; break;
}

也许一个开关稍微快一点

switch (hexchar) {
    default: /* error */; break;
    case '0': nibble = 0; break;
    case '1': nibble = 1; break;
    //...
    case 'F': case 'f': nibble = 15; break;
}
Boost已经有了unhex算法,您可以将基准测试结果作为基线进行比较:

unhex 616263646566,输出->abcdef unhex 3332,输出->32

如果您的字符串非常庞大,那么您可以考虑使用基于OpenMP的基于线程的框架的并行方法,并行STL

Boost已经有一个UNHEX算法,您可以将基准测试结果作为基线:

unhex 616263646566,输出->abcdef unhex 3332,输出->32

如果您的字符串非常庞大,那么您可以考虑使用基于线程的框架(如OpenMP、并行STL

>P>)的一些并行方法。以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,
};
void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  for(size_t i=0; hexstr[i]!='\0'; i+=2)
  {
    *dst = LOOKUP[ hexstr[i  ] ] << 4 |
           LOOKUP[ hexstr[i+1] ];
    dst++;
  }
}
这将牺牲256字节的只读内存,反过来,我们不必做任何形式的算术。uint_fast8_允许编译器选择一个更大的类型,如果它认为这样有助于提高性能的话

完整的代码将是这样的:

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,
};
void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  for(size_t i=0; hexstr[i]!='\0'; i+=2)
  {
    *dst = LOOKUP[ hexstr[i  ] ] << 4 |
           LOOKUP[ hexstr[i+1] ];
    dst++;
  }
}
在x86_64上测试时,这归结为大约10条指令。除循环条件外,无分支。值得注意的是,检查任何内容都不会出错,因此您必须确保数据是正确的,并且在其他地方包含偶数个半字节

测试代码:

#include <stdio.h>
#include <stdint.h>

void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  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]);
  }
}

以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,
};
void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  for(size_t i=0; hexstr[i]!='\0'; i+=2)
  {
    *dst = LOOKUP[ hexstr[i  ] ] << 4 |
           LOOKUP[ hexstr[i+1] ];
    dst++;
  }
}
这将牺牲256字节的只读内存,反过来,我们不必做任何形式的算术。uint_fast8_允许编译器选择一个更大的类型,如果它认为这样有助于提高性能的话

完整的代码将是这样的:

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,
};
void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  for(size_t i=0; hexstr[i]!='\0'; i+=2)
  {
    *dst = LOOKUP[ hexstr[i  ] ] << 4 |
           LOOKUP[ hexstr[i+1] ];
    dst++;
  }
}
在x86_64上测试时,这归结为大约10条指令。除循环条件外,无分支。值得注意的是,检查任何内容都不会出错,因此您必须确保数据是正确的,并且在其他地方包含偶数个半字节

测试代码:

#include <stdio.h>
#include <stdint.h>

void hexstr_to_bytes (const char* restrict hexstr, uint8_t* restrict dst)
{
  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,
  };
  
  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]);
  }
}

直接回答:我不知道完美的算法。x86 asm:根据《英特尔性能指南》-展开循环。请尝试XLAT指令2所需的不同表[消除条件分支]。修改调用接口以包含显式块长度,因为调用方应该知道字符串长度[消除strlen]。测试输出数组空间是否足够大:小错误-记住奇数长度除以2是向下舍入的。因此,如果源的长度为奇数,则仅初始化输出的最后一个字节。将return从void更改为int类型,以便可以传递错误或成功代码以及处理的长度。处理空长度输入。在块中执行的优点是,实际限制变成了操作系统文件大小限制。尝试设置线程关联。我怀疑性能上的限制最终取决于RAM到CPU总线。如果是这样,请尝试在RAM支持的最大位宽度上进行数据提取和存储。在C或C++中进行编码时,如果没有优化和更高的水平,则进行台架测试。通过执行反向过程,然后逐字节比较非零机会CRC-32未命中来测试有效性。PBYTE可能有问题-使用本机c无符号字符类型。代码大小和一级缓存未命中数与循环释放量之间有一个有待测试的权衡。在asm中,使用cx/ecx/rcx进行倒计时,而不是通常的倒计时和比较。假设CPU支持,SIMD也是可能的。

直接回答:我不知道完美的算法。x86 asm:根据《英特尔性能指南》-展开循环。请尝试XLAT指令2所需的不同表[消除条件分支]。修改调用接口以包含显式块长度,因为调用方应该知道字符串长度[消除strlen]。测试输出数组空间是否足够大:小错误-记住奇数长度除以2是向下舍入的。因此,如果源的长度为奇数,则仅初始化输出的最后一个字节。将return从void更改为int类型,以便可以传递错误或成功代码以及处理的长度。处理空长度输入。在块中执行的优点是,实际限制变成了操作系统文件大小限制。试试塞蒂 ng线程亲和力。我怀疑性能上的限制最终取决于RAM到CPU总线。如果是这样,请尝试在RAM支持的最大位宽度上进行数据提取和存储。在C或C++中进行编码时,如果没有优化和更高的水平,则进行台架测试。通过执行反向过程,然后逐字节比较非零机会CRC-32未命中来测试有效性。PBYTE可能有问题-使用本机c无符号字符类型。代码大小和一级缓存未命中数与循环释放量之间有一个有待测试的权衡。在asm中,使用cx/ecx/rcx进行倒计时,而不是通常的倒计时和比较。假设CPU支持,SIMD也是可能的

当输入十六进制字符串非常大时,例如1000000长度

事实上,1兆对于今天的电脑来说并不是那么长

如果您需要能够处理更大的字符串,比如10s的千兆字节,甚至是大量的1MEG字符串,您可以使用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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 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, 0, 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);
}
虽然您的最终代码几乎肯定足以满足您的需求,但我认为这对您或未来的SO用户来说可能很有趣

当输入十六进制字符串非常大时,例如1000000长度

事实上,1兆对于今天的电脑来说并不是那么长

如果您需要能够处理更大的字符串,比如10s的千兆字节,甚至是大量的1MEG字符串,您可以使用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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 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, 0, 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);
}

虽然您的最终代码几乎可以满足您的需要,但我认为这对您或未来的SO用户来说可能很有趣。

您可以从不每次迭代都调用strlen开始。您是否希望使用“英特尔SIMD内部函数”(如“mm”cmpgt\u epi8或“mm”shuffle\u epi8)一次处理16或32个字节?也就是说,在另一个方向上是有效的。另外,不要称你的输出为hexBuff。这不再是以16位数为基数的对了,这就是重点。它是压缩二进制或字节流。这比平常更让人困惑。std::map的想法不错,但也不是很快。构造一个std::string,然后按字母顺序进行搜索有点慢。查找表实际上应该是一个半字符[UCHAR_MAX]。CPU非常擅长这种指针arithmetic@Msalter. 是的,我就是这个意思。数组应该只包含由ascii字符索引的十六进制值。然后你一次移动两个字符得到一个字节,例如字节b=nibbles[char0]你可以不在每次迭代中调用strlen来开始。你想使用像_mm_cmpgt_epi8或_mm_shuffle_epi8这样的英特尔SIMD内部函数一次处理16或32个字节吗?也就是说,在另一个方向上是有效的。另外,不要称你的输出为hexBuff。这不再是以16位数为基数的对了,这就是重点。它是压缩二进制或字节流。这比平常更让人困惑。std::map的想法不错,但也不是很快。构造一个std::string,然后按字母顺序进行搜索有点慢。查找表实际上应该是一个半字符[UCHAR_MAX]。CPU非常擅长这种指针arithmetic@Msalter. 是的,我就是这个意思。数组应该只包含由ascii字符索引的十六进制值。然后你一次移动两个字符得到一个字节,例如字节b=nibbles[char0]不是我的下一票,但这可能会慢一些。现代CPU,即本世纪的CPU,对分支非常敏感,以至于好的编译器会尝试找出这样的开关,并用查找表来代替它们。但是,如果你可以直接编写查找表,为什么还要依赖编译器呢?@MSalters:事实上,如果它像我担心的那样编译成跳转表,那么这就是垃圾。幸运的是,GCC-O2足够聪明,可以首先通过范围检查将其编译成一个查找表,至少在我编写的版本中,它只使用大写字母处理,就像源代码一样,尽管这可能会使表变小。根据Godbolt,GGC4.1和GCC4.4之间的某个时间。对于代码样式,是的,我同意手动编写LUT会更好,比如c-='0'/如果c试图避免ASCII假设,那么可以使用数组初始值设定项,使用指定的初始值设定项语法,比如uint8_t LUT[]={0,['1'-'0']=1,['f'-'0']=15};。这可能比一个开关更复杂,所以如果你的编译器足够聪明,这并没有那么糟糕,否则除非数据有简单的模式,否则在大多数字符上都会出现分支预测失误。顺便说一句,以前的Godbolt链接使用的是unsigned int输出:我在测试了一些东西后忘记将它放回unsigned char。GCC10.3-O3是否将此开关转换为数据查找表。而不是我的downvot

e、 但这可能要慢一些。现代CPU,即本世纪的CPU,对分支非常敏感,以至于好的编译器会尝试找出这样的开关,并用查找表来代替它们。但是,如果你可以直接编写查找表,为什么还要依赖编译器呢?@MSalters:事实上,如果它像我担心的那样编译成跳转表,那么这就是垃圾。幸运的是,GCC-O2足够聪明,可以首先通过范围检查将其编译成一个查找表,至少在我编写的版本中,它只使用大写字母处理,就像源代码一样,尽管这可能会使表变小。根据Godbolt,GGC4.1和GCC4.4之间的某个时间。对于代码样式,是的,我同意手动编写LUT会更好,比如c-='0'/如果c试图避免ASCII假设,那么可以使用数组初始值设定项,使用指定的初始值设定项语法,比如uint8_t LUT[]={0,['1'-'0']=1,['f'-'0']=15};。这可能比一个开关更复杂,所以如果你的编译器足够聪明,这并没有那么糟糕,否则除非数据有简单的模式,否则在大多数字符上都会出现分支预测失误。顺便说一句,以前的Godbolt链接使用的是unsigned int输出:我在测试了一些东西后忘记将它放回unsigned char。是GCC10.3-O3,使用此开关将其转换为数据查找表。OP的字符串长度仅为1MiB左右;几乎不值得启动线程,除非您在多个缓冲区/重复上有很多这样的事情要做。一个经过良好调优的unhex,尤其是使用SIMD intrinsic时,应该在每个时钟周期的输出中产生多个字节。@PeterCordes这是真的,对于大约1MB的字符串大小,不值得使用线程。所以我说是非常大的弦。SIMD始终是字符串处理的首选。OP的字符串长度只有1MiB左右;几乎不值得启动线程,除非您在多个缓冲区/重复上有很多这样的事情要做。一个经过良好调优的unhex,尤其是使用SIMD intrinsic时,应该在每个时钟周期的输出中产生多个字节。@PeterCordes这是真的,对于大约1MB的字符串大小,不值得使用线程。所以我说是非常大的弦。SIMD始终是字符串处理的首选。在使用查找表时,他还可以使用64K大小的查找表,并将字符串视为uint16的数组,每个uint16元素表示两个十六进制数字……另一种选择是使用两个数组;一个值已经移位的。这将在不使用64k内存的情况下在循环中节省一个移位。@MartinRosenau:是的,你可以,但是会更快吗?可能地您将有16或32条热缓存线,它们之间的间隔为256字节。预热成本很高,但如果用于大型缓冲区,则可能会在缓存中保持热状态,足以使大型1MiB缓冲区获胜。尽管如此,如果您要达到这一水平,您可能只需要为一些您关心的ISA(如x86和ARM)编写SIMD Intrinsic实现,而且速度更快,每个输出周期产生的字节数超过1个,可能在现代x86上使用SSE4的4B/c还没有充分考虑过设计。如果没有无符号字符,查找[hexstr][i]]hextr[i]<0.forsize\u t i=0时的风险;hextr[i]!='\0';i+=2是具有奇数长度字符串的UB。现在,请参见文本中的注释,而不是代码中的注释。在使用查找表时,他还可以使用64K大小的查找表,并将字符串视为表示两个十六进制数字的每个uint16_t元素的uint16_t数组……另一种选择是使用两个数组;其中一个数组的值已移位。这将是多余的不使用64k内存的循环切换。@MartinRosenau:可以,但速度会更快吗?可能;您会有16或32条热缓存线,它们之间的间隔为256字节。预热成本很高,但如果用于大型缓冲区,则可能会在缓存中保持热状态,足以赢得大型1MiB缓冲区。不过,如果您要为了达到这一水平,您可能只需要为您关心的某些ISA(如x86和ARM)编写SIMD Intrinsic实现,而且速度更快,每周期输出的字节数超过1个,也许在现代x86上使用SSE4的4B/c还没有完全考虑过设计。如果没有无单字字符,查找[hexstr[i]]当hextr[i]<0.forsize\u t i=0时的风险;hextr[i]!='\0';i+=2是具有奇数长度字符串的UB。现在请参见文本中的注释,而不是代码中的注释使用标量逐字节代码,即使使用查找表,也不太可能在内存带宽上出现瓶颈。每2个时钟周期,输入的速度不会超过2个字节加上输出的1个字节,因此,这仅是时钟速度GB/sec的1.5倍,或每秒6GB/s4GHz的CPU。这可能接近于大型Xeon上可用的单核带宽,尤其是当其他内核也很忙时,但如果它是现代桌面上唯一的东西,内存带宽更像是40GB/s。xlat在这里是无用的;它在AMD(包括Zen)上的价格为2 UOP,在Intel Sandybridge系列上的价格为3 UOP,因此它并不比movzx edx、al/m便宜oval,[rbx+r
dx]或任何其他临时注册表,而不是RDX。手动执行时,通常可以通过将字节值设置为零扩展来避免初始movzx,例如,从内存加载movzx或右移。除x86标记外,所有值都表示8088到当前的Intel、AMD、Cyrix。所以XLAT对于OP来说可能是一个有用的建议。至于内存带宽,我写这个框的限制是1.33事务秒x16字节=20 G/s。但如果一次获取存储字节???我的其余答案直接来自《英特尔性能手册》,除了关于要释放多少循环的部分。不幸的是,如果使用优化C或C++,所有这些都超出了OP控制。此外,6种模式中的3种5记录+不真实仍然是8088,我们不知道OP的目标…querent提到他们的CPU是i7-8700,3.2GHz,因此暗示他们在谈论现代x86。这使得xlat在任何模式下都没有用处。除非他们说不,例如[X86-16]标签,正常人应该假定他们是64位模式,具有现代C或C++编译器,可能具有32位模式的可移植性。此外,当输入十六进制字符串非常大时,例如1000000长度,也会排除实模式,因此8086/8088/186,因为这将用输入填充整个1MiB物理地址空间。您的答案中有一些好东西,这就是为什么我没有投反对票。最好将文章分成几段,如果你想谈谈8086/286的调优,那么就把它放在一个单独的章节中,并用一个标题指出这一点。即使使用查找表,你也不太可能用每次字节的标量代码来限制内存带宽。每2个时钟周期的速度不会超过2个字节的输入加上1个字节的输出,因此,对于4GHz CPU来说,这只是时钟速度GB/sec的1.5倍,或者说是6GB/s。这可能接近于大型Xeon上可用的单核带宽,尤其是当其他核心也很忙时,但如果它是现代桌面上唯一的东西,那么内存带宽更像是40GB/s。xlat在这里是无用的;它在AMD(包括Zen)上的价格为2 UOP,在Intel Sandybridge系列上的价格为3 UOP,因此它并不比movzx edx、al/mov al、[rbx+rdx]或任何其他temp reg(而非rdx)便宜。手动执行时,通常可以通过将字节值设置为零扩展来避免初始movzx,例如,从内存加载movzx或右移。除x86标记外,所有值都表示8088到当前的Intel、AMD、Cyrix。所以XLAT对于OP来说可能是一个有用的建议。至于内存带宽,我写这个框的限制是1.33事务秒x16字节=20 G/s。但如果一次获取存储字节???我的其余答案直接来自《英特尔性能手册》,除了关于要释放多少循环的部分。不幸的是,如果使用优化C或C++,所有这些都超出了OP控制。此外,6种模式中的3种5记录+不真实仍然是8088,我们不知道OP的目标…querent提到他们的CPU是i7-8700,3.2GHz,因此暗示他们在谈论现代x86。这使得xlat在任何模式下都没有用处。除非他们说不,例如[X86-16]标签,正常人应该假定他们是64位模式,具有现代C或C++编译器,可能具有32位模式的可移植性。此外,当输入十六进制字符串非常大时,例如1000000长度,也会排除实模式,因此8086/8088/186,因为这将用输入填充整个1MiB物理地址空间。您的答案中有一些好东西,这就是为什么我没有投反对票。最好分成几段,如果你想说一些关于8086/286的调优的话,就把它放在一个单独的章节中,并用一个标题指出这一点。另一个SIMD实现,使用vpmaddubsw _mm256 _maddubs_epi16来水平组合相邻字节的半字节,而不是shift/OR。这可能会很好地为vpackusswb+vpermq设置,将两个32字节的向量合并为一个。或者在vpshufb之后,您可以使用vpunpcklqdq+vpermq创建一个32字节的输出向量,总共进行4次随机洗牌。Hrm,这仍然是每128位输出2次洗牌,只节省1个存储。在1MiB缓冲区上重复运行应该比更大的缓冲区从SIMD中受益更多。安装在三级缓存甚至二级缓存中,可以为SIMD指令提供足够的数据以使其全速运行,从而显示出相对于标量每次字节查找表的全部优势。哦,实际上,ZachB的快速十六进制使用pmaddubsw实现十进制值=9*ascii>>6+ascii&0xf;每个十六进制数字。这是一个聪明的标量映射函数,但与普通比较相比,它可能不值得将字节解压为单词。我在第一条评论中建议的是,用一个替代品代替你的方式,这样会更好,以OoO exec可以隐藏的延迟为代价节省1个uop。@PeterCordes-所以我试着提取zbjornson的代码
加入我的样本。虽然它给了我同样的答案,但它比我的要慢一点耶!。至于用maddubs替换vpsllq+vpor,它确实快了一点点。我试过玩_mm256_unpaclo_epi64ymm1,ymm1,但我不知道这是怎么回事。我自己也在卡比湖上,vpmaskmovq比vpermq+vmovdqu稍微好一点。根据英特尔文档:例如,如果掩码位均为零,则不会检测到任何故障,因此,如果使用正确,即使有越界地址,也不会出现任何故障。目前还不清楚OP的硬件要求可能只是一个学校项目,所以我可能会把答案中的内容放在一边。如果某些未来用户需要更好的性能,请考虑用maddubs替换vpsllq+vpor,用vpmaskmovq替换vpermq+vmovdqu。另一个SIMD实现使用vpmaddubsw_mm256_maddubs_epi16来水平组合相邻字节的半字节,而不是移位/或。这可能会很好地为vpackusswb+vpermq设置,将两个32字节的向量合并为一个。或者在vpshufb之后,您可以使用vpunpcklqdq+vpermq创建一个32字节的输出向量,总共进行4次随机洗牌。Hrm,这仍然是每128位输出2次洗牌,只节省1个存储。在1MiB缓冲区上重复运行应该比更大的缓冲区从SIMD中受益更多。安装在三级缓存甚至二级缓存中,可以为SIMD指令提供足够的数据以使其全速运行,从而显示出相对于标量每次字节查找表的全部优势。哦,实际上,ZachB的快速十六进制使用pmaddubsw实现十进制值=9*ascii>>6+ascii&0xf;每个十六进制数字。这是一个聪明的标量映射函数,但与普通比较相比,它可能不值得将字节解压为单词。我在第一条评论中建议的是,用一种替代你的方式,这种方式应该更好,以OoO exec可以隐藏的延迟为代价节省1 uop。@PeterCordes-所以我尝试将zbjornson的代码引入我的示例中。虽然它给了我同样的答案,但它比我的要慢一点耶!。至于用maddubs替换vpsllq+vpor,它确实快了一点点。我试过玩_mm256_unpaclo_epi64ymm1,ymm1,但我不知道这是怎么回事。我自己也在卡比湖上,vpmaskmovq比vpermq+vmovdqu稍微好一点。根据英特尔文档:例如,如果掩码位均为零,则不会检测到任何故障,因此,如果使用正确,即使有越界地址,也不会出现任何故障。目前还不清楚OP的硬件要求可能只是一个学校项目,所以我可能会把答案中的内容放在一边。如果未来的用户需要更好的性能,可以考虑用maddubs替换vpsllq+vpor,用vpmaskmovq替换vpermq+vmovdqu。