有没有一种算法可以快速将大量的十六进制字符串转换为字节流?使用汇编语言/C/C++。

3

这是我的当前代码:

//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: 感谢Paddy的评论。 我太粗心了,没有注意到strlen(时间: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++;
    }
}

顺便说一下,非常感谢你们。你们真的很棒!是真正的研究人员!


10
可以尝试避免在每次迭代中都调用strlen函数。 - paddy
1
你想使用英特尔SIMD内置函数,如 _mm_cmpgt_epi8_mm_shuffle_epi8 一次处理16或32字节吗? 这对于 int->hex 字符串非常有效,可以参考 这里,同时也可以在其他方向上发挥作用。 - Peter Cordes
1
此外,不要将您的输出称为hexBuff。这已经不是一堆16进制数字对了,这正是重点所在。它是打包的二进制数据流,这会更加混淆。 - Peter Cordes
1
std::map 的想法很好,但速度不太快。构建一个 std::string 然后进行字母搜索有点慢。查找表真的应该是一个 char nibbles[UCHAR_MAX]。CPU 在这种指针算术方面非常擅长。 - MSalters
1
@Msalter。是的,这就是我的意思。数组应该只包含由ASCII字符索引的十六进制值。然后你每次移动两个字符来获取一个字节,例如 byte b = (nibbles[char0] << 4) | nibbles[char1]; - Emanuel P
显示剩余11条评论
5个回答

5
创建最高效的代码(以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, ['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条指令(Godbolt)。除了循环条件外,无需分支。值得注意的是,没有任何错误检查,因此您必须确保数据在其他地方正确(并包含偶数个半字节)。
测试代码:
#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]);
  }
}

1
在使用查找表时,他还可以使用一个大小为64K的查找表,并将字符串视为uint16_t数组(每个uint16_t元素表示两个十六进制数字...)。 - Martin Rosenau
2
另一种选择是使用两个数组;一个已经进行了值的移位。这将在循环中节省一个移位,而不使用64k内存。 - Emanuel P
1
@MartinRosenau: 是的,你可以这样做,但会更快吗?可能性很大。你将拥有16或32个“热”缓存行,它们之间分布有256字节步长。预热成本高昂,但如果用于巨大的缓冲区,则可能保持足够的缓存热度,以便在1MiB缓冲区中取得胜利。不过,如果你要付出这种程度的努力,为一些你关心的ISA(如x86和ARM)编写SIMD指令实现,甚至可以更快地运行,并且产生比每个周期输出1字节更好的结果,也许是4B/ c使用SSE4在现代x86上(尽管还没有完全思考设计)。 - Peter Cordes
1
如果没有一个(unsinged char),当hexstr[i] < 0时,LOOKUP[ hexstr[i] ]存在未定义行为。 - chux - Reinstate Monica
1
“for(size_t i=0; hexstr[i]!='\0'; i+=2)” 在处理长度为奇数的字符串时会导致未定义行为。 (现在请注意文本中的注释,但代码中并没有) - chux - Reinstate Monica
显示剩余13条评论

3
当输入的十六进制字符串非常大(比如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, 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);
}

需要注意的几点:

  • 此处没有错误处理。我没有检查内存溢出或文件读取错误。我甚至没有检查输入流中的无效字符或小写十六进制数字。
  • 这段代码假设字符串的大小是可用的,而不必遍历字符串(在这种情况下为ftelli64)。如果您需要逐字节遍历字符串以获取其长度(如strlen),则可能已经失去了优势。
  • 我保留了HexString2Bytes,这样您就可以比较我的代码与您的代码的输出,以确保我正确地进行转换。
  • HexString2BytesSSE假定字符串中的字节数可以均匀地被32整除(一个值得怀疑的假设)。然而,重新设计它以调用HexString2Bytes来处理最后(最多)31个字节相当容易,并且不会对性能产生太大影响。
  • 我的test.txt有2GB长,这段代码运行了16次。这是差异变得明显所需的时间。

对于想要插话的人(因为当然你想要),这是最内层循环的汇编输出以及一些注释:

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,ymm2,ymm0  ; Adjust all the 'A'

; Combine the nibbles to form bytes
110C  vpsllq      ymm1,ymm2,0Ch   ; Shift nibble up + align nibbles
1111  vpor        ymm0,ymm1,ymm2  ; Combine lower and upper nibbles

; Coalesce the odd numbered bytes
1115  vpshufb     ymm2,ymm0,ymm7

; Since vpshufb can't cross lanes, use vpermq to
; put all 16 bytes together
111A  vpermq      ymm3,ymm2,8

1120  vmovdqu     xmmword ptr [rax-10h],xmm3
1125  sub         rdx,1
1129  jne         main+0F0h (10F0h)

虽然你的最终代码很可能已经满足了你的需求,但我认为这对你(或未来的 SO 用户)可能是有趣的。


在更小的转换(适合L2甚至L1d)周围进行更多的重复循环迭代,可能会看到更大的差异。关于_mm256_unpacklo_epi64 - 不是使用相同的参数两次,而是使用两个不同的参数,作为将2个向量的低qword组合起来的一种方式,然后使用vpermq生成完整的32字节向量。但是正如我所说的那样,这需要每个输入都有一个vpshufb,因此它比使用vpackuswb + vpermq组合向量更糟糕,特别是如果每个字中没有需要在vpmaddubsw之后清除的高垃圾。 - Peter Cordes
@PeterCordes “L2” - 是的,但那只是更傻了。我没有看到任何保持顺序的方法来获取这里的2个向量,所以我仍然没有看到使用解包的方法。但是:使用_mm256_maskstore_epi64怎么样(将vpermq + vmovdqu组合为1个指令)?如果我洗牌字节,使它们存储在中间 128位中,那么我可以设置掩码以跳过写入第一个和最后一个i64。这确实意味着传递一个指针,该指针位于缓冲区的之前,然后设置掩码表示不要在那里写入。英特尔文档说这应该是合法的,但感觉像UB。 - David Wohlferd
我建议使用内部通道混洗将2个向量混洗在一起(因为AVX1/2不好用,没有AVX-512的vpermt2q),然后使用vpermq对结果进行排列,以使所有8字节块处于正确的最终顺序。是的,掩码存储是一个很好的想法;在ISO C中,形成一个指针,它不指向数组内部(或者指向数组结束位置之后的一个位置)是技术上未定义的行为,但实际上支持英特尔指令集的真实世界实现已经定义了这种行为。掩码存储的故障抑制使其变得安全,尽管如果实际需要故障抑制,它可能会变慢。 - Peter Cordes
不幸的是,在Zen/Zen2上,vpmaskmovq 存储的速度非常慢,因此对于便携式性能来说并不是一个很好的方案(请查看 AVX2: vpmskmov (M 在 https://uops.info/ 上)。在SKX / ICL上,它们在前端的成本为3个uop:p0 ALU,加上存储地址和存储数据 uop(它们不进行微融合)。只有AVX-512具有单uop掩码存储。它避免了端口5洗牌瓶颈,但与前端吞吐量相比差劲。即使如此,如果您的输出指针位于页面的开头,并且前一页未映射,则会出现数百个周期的微代码帮助,根据我所知。 - Peter Cordes
2
我自己使用的是Kaby Lake,vpmaskmovq比vpermq + vmovdqu效果稍好。根据英特尔文档:例如,如果掩码位全部为零,则不会检测到任何故障,因此如果正确使用,即使使用“越界”地址,也不应该出现任何故障。不清楚OP的硬件要求是什么(可能只是一个学校项目),所以我可能会保留答案中的内容。如果将来有用户需要更好的性能,请考虑用maddubs替换vpsllq+vpor,并用vpmaskmovq替换vpermq + vmovdqu。 - David Wohlferd
显示剩余5条评论

1
Boost已经有了一个名为unhex的算法实现,您可以将其作为基准来比较基准测试结果:
unhex ( "616263646566", out )  --> "abcdef"
unhex ( "3332", out )          --> "32"

如果您的字符串非常大,那么您可以考虑一些并行处理方法(使用基于线程的框架,如OpenMP,parallel STL)。

2
OP的字符串长度只有约1MiB,除非您需要处理大量此类操作(在多个缓冲区/重复执行),否则几乎不值得启动线程。经过良好调整的unhex,特别是使用SIMD内置函数,应该能够每个时钟周期产生多个字节的输出。 - Peter Cordes
1
@PeterCordes 这是真的,对于大约1MB大小的字符串,使用线程是不值得的。因此,我说对于非常庞大的字符串。SIMD始终是字符串处理的首选。 - prehistoricpenguin

1
也许使用开关会(稍微)更快。
switch (hexchar) {
    default: /* error */; break;
    case '0': nibble = 0; break;
    case '1': nibble = 1; break;
    //...
    case 'F': case 'f': nibble = 15; break;
}

不是我点的踩,但这个可能会比较慢。现代 CPU(即本世纪)对分支非常敏感,好的编译器会尝试查找这些开关并用查找表替换它们。但如果可以编写查找表,为什么要依赖编译器呢? - MSalters
@MSalters:确实,如果它编译成了我担心的跳表,那将是垃圾。幸运的是,GCC -O2 确实足够聪明地将其编译为查找表(首先进行范围检查),至少在我只处理类似源代码的大写字母版本中是如此,尽管这可能只会使表更小。(根据Godbolt,介于GGC4.1和GCC4.4之间。https://godbolt.org/z/rfb9x51Gf)。对于代码风格,是的,我同意手动编写LUT会更好,例如 c -= '0' / if (c <= 'f' - '0') ...,特别是如果您假定ASCII。 - Peter Cordes
如果您试图避免ASCII假设,可以使用指定初始化器语法的数组初始化器,例如uint8_t lut[] = { 0, ['1' - '0'] = 1, ..., ['f' - '0'] = 15 };。这甚至可能比switch更复杂,因此如果您的编译器足够聪明,则不会有太大问题,否则除非数据具有简单模式,否则大多数字符都会出现分支预测错误。 - Peter Cordes
顺便提一下,之前的Godbolt链接使用了“unsigned int”输出:我在测试某些东西后忘记将其改回“unsigned char”。https://godbolt.org/z/an3hvecT4是GCC10.3 -O3,并将此开关转换为数据查找表。 - Peter Cordes

0
直接回答:我不知道完美的算法。 x86汇编:根据英特尔性能指南,解开循环。尝试使用XLAT指令(需要2个不同的表)[消除条件分支]。修改调用接口以包括显式块长度,因为调用者应该知道字符串长度[消除strlen()]。测试输出数组空间是否足够大:小错误-请记住,奇数长度除以二是向下取整。因此,如果源长度为奇数,请初始化输出的最后一个字节(仅)。从void更改返回类型为int,这样您就可以传递错误或成功代码和处理的长度。处理零长度输入。分块处理的优点是实际限制成为操作系统文件大小限制。尝试设置线程亲和性。我怀疑性能限制的极限最终是RAM到CPU总线,具体取决于情况。如果是这样,请尝试在RAM支持的最大位宽上进行数据获取和存储。如果在C或C ++中进行编码,则无需优化进行台阶测试。通过执行反向过程,然后进行字节比较(CRC-32错过的非零机会),测试有效性。PBYTE可能存在问题-使用本机c unsigned char类型。代码大小和L1之间存在可测试的权衡-缓存未命中次数与展开多少循环。在汇编中,使用cx / ecx / rcx进行倒计数(而不是通常的计数和比较)。如果CPU支持,也可以使用SIMD。

使用标量逐字节代码,即使使用查找表,也不太可能出现内存带宽瓶颈。每2个时钟周期,您的速度不会超过2个输入字节加1个输出字节,因此每秒只有大约1.5倍的时钟速度GB / s,或者对于4GHz CPU而言为6GB / s。这可能接近大型Xeon上可用的单核带宽(特别是当其他核心也很忙时),但如果它是现代桌面上唯一的东西,则内存带宽更接近40GB / s。 - Peter Cordes
1
在这里使用 xlat 是无益的;它在 AMD(包括 Zen)上花费 2 uops,在 Intel Sandybridge 家族上花费 3 uops(https://uops.info/,https://agner.org/optimize/),因此它和 movzx edx, al / mov al, [rbx + rdx](或者其他临时寄存器代替 RDX)一样昂贵。手动执行时,通常可以通过安排字节值已被零扩展(例如从内存中的 movzx 装载或右移)来避免初始的 movzx - Peter Cordes
除了x86标签暗示着从8088到当前的Intel、AMD、Cyrix等所有内容,其他都是正确的。因此,XLAT可能是OP的一个有用建议。至于内存带宽,我所使用的计算机限制在1.33个事务每秒×16字节=20 GB/s。但如果逐字节获取存储?我的其余答案直接来自英特尔性能手册,除了解开循环的数量。不幸的是,如果使用优化的C或C ++,所有这些都大多超出了OP的控制范围。此外,六种模式中的三种(5种文档+“虚幻”)仍然是8088,我们不知道OP的目标是哪一种... - sys101
询问者提到他们的CPU是i7-8700,3.2GHz,因此暗示他们正在谈论现代x86。这使得在任何模式下都不适用于xlat。除非他们另有说明(例如没有[x86-16]标签),否则普通人应该认为他们的意思是使用现代C或C ++编译器的64位模式,可能可移植到32位模式。此外,“当输入十六进制字符串非常大(例如1000000长度)”也排除了实模式(因此排除了8086/8088/186),因为这将仅使用输入填充整个1MiB物理地址空间。 - Peter Cordes
但如果每次只获取一个字节,那么缓存就会发挥作用,并且让您每个时钟周期可以执行高达2个加载和1个存储到L1d缓存(自Haswell或Zen2以来,包括AGU限制),只需要硬件预取即可跟上触及新的64字节行,总带宽可能达到大约时钟速度的1.5倍。 - Peter Cordes
1
你的回答中有一些好东西,这就是为什么我没有给你点踩的原因。如果你想要说一些关于8086/286调优的话,最好将其放在一个单独的部分,并使用“###标题”来指出。同时,将文本编辑成段落也是很不错的建议。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接