使用ASCII字符编码在C#中将字符串转换为byte []数组的最快方式(性能方面)

Nos*_*ama 8 c# string performance byte ascii

在C#中将字符串转换为byte []数组的最快方法是什么?我通过套接字发送大量的字符串数据,需要优化每个操作.目前我在使用以下命令发送之前将字符串转换为byte []数组:

private static readonly Encoding encoding = new ASCIIEncoding();
//...
byte[] bytes = encoding.GetBytes(someString);
socket.Send(bytes);
//...
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 15

如果您的所有数据确实都是ASCII,那么您可以稍微快一些ASCIIEncoding,它具有各种(完全合理的)错误处理位等.您也可以通过避免创建新字节来加快速度.数组一直都是.假设您有一个上限,您的所有消息都将在其下面:

void QuickAndDirtyAsciiEncode(string chars, byte[] buffer)
{
    int length = chars.Length;
    for (int i = 0; i < length; i++)
    {
        buffer[i] = (byte) (chars[i] & 0x7f);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你会做类似的事情:

readonly byte[] Buffer = new byte[8192]; // Reuse this repeatedly
...
QuickAndDirtyAsciiEncode(text, Buffer);
// We know ASCII takes one byte per character
socket.Send(Buffer, text.Length, SocketFlags.None);
Run Code Online (Sandbox Code Playgroud)

这是非常绝望的优化.我ASCIIEncoding一直坚持,直到我证明这是瓶颈(或者至少这种糟糕的黑客行为没有帮助).


jri*_*sta 9

我会说你现在这样做是多么好.如果您真的担心这种非常低级别的优化,我可以做的最好的建议就是获得Reflector.使用反射器,您可以自己查看代码(大多数时间),并查看算法是什么.如果反射器没有显示,您可以随时下载Microsofts SSCLI(共享源公共语言基础结构)以查看MethodImplOptions.InternalCall方法背后的C++代码.

作为参考,这里是Encoding.ASCII.GetBytes的实际实现:

public override int GetBytes(string chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
{
    if ((chars == null) || (bytes == null))
    {
        throw new ArgumentNullException();
    }
    if ((charIndex < 0) || (charCount < 0))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((chars.Length - charIndex) < charCount)
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((byteIndex < 0) || (byteIndex > bytes.Length))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((bytes.Length - byteIndex) < charCount)
    {
        throw new ArgumentException();
    }
    int num = charIndex + charCount;
    while (charIndex < num)
    {
        char ch = chars[charIndex++];
        if (ch >= '\x0080')
        {
            ch = '?';
        }
        bytes[byteIndex++] = (byte) ch;
    }
    return charCount;
}
Run Code Online (Sandbox Code Playgroud)


Gle*_*den 6

\n

使用 SIMD 寄存器实现通用 memcpy 库函数的性能特征明显比使用通用寄存器的等效实现更加丰富多彩...

\xc2\xa0 - Intel 64 和 IA-32 架构优化参考手册\n(2018 年 4 月)\xc2\xa73.7.6.1

\n
\n

为了在 8 位和“宽”(16 位,Unicode)文本之间转换中型到大型数据块的速度惊人byte[],您需要考虑部署SIMD指令PUNPCKLBW+ PUNPCKHBW(加宽)和PACKUSWB(缩小)。在.NET中,这些可用作 x64 JIT 内在函数,针对硬件加速System.Numerics类型发出VectorVector<T>请参阅此处了解更多信息)。通用版本Vector<T>在包中定义System.Numerics.Vectors,目前仍在相当活跃的开发中。如下所示,您可能还需要包含该System.Runtime.CompilerServices.\xe2\x80\x8bUnsafe包,因为这是作者推荐的首选 SIMD 加载/存储技术Vector<T>

\n

相关的 SIMD 加速仅针对x64 模式下的 CPU 启用,但除此之外,.NET 为库中的模拟代码提供透明的回退System.Numerics.Vectors,因此此处演示的代码确实可以在更广泛的 .NET 生态系统中可靠地运行,但可能会降低性能。为了测试下面所示的代码,我在x64 (SIMD) 和x86(模拟)模式下的完整.NET Framework 4.8(“桌面”)上使用了控制台应用程序。

\n

由于我不想剥夺任何人学习相关技术的机会,因此我将用C# 7Vector.Widen说明byte[]方向。从这个例子来看,执行相反的操作即,用于实现缩小方向)是简单的,并且留给读者作为练习。char[]Vector.Narrow

\n
\n

警告:
此处建议的方法完全不支持编码,它们只是将原始字节剥离/扩展(或缩小/加宽)到原始字节或从原始字节转换为原始字节,而不考虑字符映射、文本编码或其他语言属性。当加宽时,多余的字节被设置为零,当缩小时,多余的字节被丢弃。

\n
\n

其他人讨论了n\xcc\xb2u\xcc\xb2m\xcc\xb2e\xcc\xb2r\xcc\xb2o\xcc\xb2u\xcc\xb2s\xcc\xb2 h\xcc\xb2a\xcc\xb2z\xcc\xb2a \xcc\xb2r\xcc\xb2d\xcc\xb2s\xcc\xb2与本页和其他地方的此实践相关,因此请仔细检查并理解此操作的性质,然后再考虑它是否适合您的情况。为了清楚起见,下面显示的代码示例中省略了内联验证,但可以将其添加到最内层循环,同时对 SIMD 优势的影响最小。

\n

你被警告了。尽管不是 SIMD 加速,但建议对几乎所有实际应用场景Encoding使用合适实例的规范技术。尽管OP特别要求最高性能的解决方案,但首先我将总结通常应该使用的适当认可的技术。

\n
\n

要将字节数组扩展为 .NET String,请在合适的面向字节的编码实例上调用GetString()方法:

\n
\n
String Encoding.ASCII.GetString(byte[] bytes)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

要将 .NET 缩小String为(例如,Ascii)字节数组,请在合适的面向字节的编码实例上调用GetBytes()方法:

\n
\n
byte[] Encoding.ASCII.GetBytes(char[] chars)\n
Run Code Online (Sandbox Code Playgroud)\n

好的,现在到了有趣的部分——支持 SIMD(“矢量化”)的极快C#代码,用于字节数组的“哑”扩展。提醒一下,以下是一些应该引用的依赖项:

\n
// ... \nusing System.Numerics;                  // nuget: System.Numerics.Vectors\nusing System.Runtime.CompilerServices;  // nuget: System.Runtime.CompilerServices.Unsafe\n// ... \n
Run Code Online (Sandbox Code Playgroud)\n

这是公共入口点包装函数。char[]如果您更喜欢返回而不是返回的版本String,可以在本文末尾提供。

\n
/// <summary>\n/// \'Widen\' each byte in \'bytes\' to 16-bits with no consideration for\n/// character mapping or encoding.\n/// </summary>\npublic static unsafe String ByteArrayToString(byte[] bytes)\n{\n    // note: possible zeroing penalty; consider buffer pooling or \n    // other ways to allocate target?\n    var s = new String(\'\\0\', bytes.Length);\n\n    if (s.Length > 0)\n        fixed (char* dst = s)\n        fixed (byte* src = bytes)\n            widen_bytes_simd(dst, src, s.Length);\n    return s;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

接下来是主要工作循环体。请注意序言循环,如有必要,通过按字节复制最多 15 个源字节,将目标与 16 字节内存边界对齐。这确保了主“ quad-quadwise ”循环的最有效操作,该循环通过一对 SIMDPUNPCKLBW/PUNPCKHBW指令一次写入 32 字节(获取 16 个源字节,然后存储为占用 32 个字节的 16 个宽字符)。

\n

预对齐到四边形物理边界\xe2\x80\x94,如果目标地址未共同对齐\xe2\x80\x94(如此处所示),则优先到达目标地址dst而不是源地址src(如此处所示)都是来自上面引用的英特尔手册。当然,无论对齐操作如何,当主循环完成时,一次 16 字节的任何分块传输都可能留下 0 到 15 个剩余尾部字节;这些都是由一个简短的尾声循环完成的。

\n
static unsafe void widen_bytes_simd(char* dst, byte* src, int c)\n{\n    for (; c > 0 && ((long)dst & 0xF) != 0; c--)\n        *dst++ = (char)*src++;\n\n    for (; (c -= 0x10) >= 0; src += 0x10, dst += 0x10)\n        Vector.Widen(Unsafe.AsRef<Vector<byte>>(src),\n                     out Unsafe.AsRef<Vector<ushort>>(dst + 0),\n                     out Unsafe.AsRef<Vector<ushort>>(dst + 8));\n\n    for (c += 0x10; c > 0; c--)\n        *dst++ = (char)*src++;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这实际上就是全部内容了!它就像一个魅力,正如您将在下面看到的,它确实像广告中那样“尖叫” 。

\n

但首先,通过关闭 vs2017 调试器选项“禁用 JIT 优化”,我们可以检查x64 JIT 为.NET 4.7.2上的“发布”版本生成的本机 SIMD 指令流。下面是主内循环的相关部分,一次遍历 32 个字节的数据。请注意,JIT 已成功发出理论上最小的获取/存储模式。

\n
String Encoding.ASCII.GetString(byte[] bytes)\n
Run Code Online (Sandbox Code Playgroud)\n

性能测试结果:
\n我针对执行相同功能的其他四种技术测试了 SIMD 代码。对于下面列出的 .NET 编码器,这是对该方法的调用GetChars(byte[], int, int)

\n
    \n
  • 不安全字节循环的简单 C# 实现
  • \n
  • “Windows-1252”代码页的.NET 编码
  • \n
  • .NET ASCII 编码
  • \n
  • UTF-8 的 .NET 编码(无 BOM,无抛出)
  • \n
  • 本文显示的 SIMD 代码
  • \n
\n

测试包括所有测试单元的相同工作以及所有测试单元相同结果的验证。测试字节是随机的且仅限 ASCII ( [0x01 - 0x7F]),以确保所有测试单元的结果相同。输入大小是随机的,最大 1MB,log 2偏向较小的大小,因此平均大小约为 80K。

\n

为了公平起见,每次迭代的执行顺序都会系统地轮换 5 个单元。对于预热,在​​第 100 次迭代时,计时被丢弃并重置为零一次。测试工具在测试阶段不执行任何分配,并且每 10000 次迭代都会强制并等待一次完整的 GC。

\n
\n 相对刻度,标准化为最佳结果\n .NET Framework 4.7.3056.0 x64(版本)\n iter naive win-1252 ascii utf-8 simd\n-------- -------- --- ------------ ------------ ------------ ----------- \n 10000 | 131.5 294.5 186.2 145.6 100.0\n 20000 | 137.7 305.3 191.9 149.4 100.0\n 30000 | 139.2 308.5 195.8 151.5 100.0\n 40000 | 141.8 312.1 198.5 153.2 100.0\n 50000 | 142.0 313.8 199.1 154.1 100.0\n 60000 | 140.5 310.6 196.7 153.0 100.0\n 70000 | 141.1 312.9 197.3 153.6 100.0\n 80000 | 141.6 313.7 197.8 154.1 100.0\n 90000 | 141.3 313.7 197.9 154.3 100.0\n 100000 | 141.1 313.3 196.9 153.7 100.0\n \ngcServer=False; LatencyMode.Interactive;Vector.IsHardwareAccelerated=True \n
\n

在首选x64平台上,当启用 JIT 优化并且 SIMD 可用时,没有竞争。SIMD 代码的运行速度比下一个竞争者快约 150%。通常Encoding.Default是“Windows-1252”代码页,其性能特别差,比 SIMD 代码慢大约 3 倍。

\n

之前我提到过测试数据大小的分布强烈对数偏向于零。如果没有这一步——意味着大小从 0 到 1,048,576 字节的均匀分布(平均测试大小 512K)——SIMD 继续领先,所有其他单元的表现与上面所示的代码相比相对较差。

\n
\nnaive 153.45%\nwin-1252 358.84%\nascii 221.38%\nutf-8 161.62%\nsimd 100.00%\n
\n

至于非 SIMD(仿真)情况,UTF-8 和 SIMD 非常接近(通常彼此相差 3-4%),并且比其他的要好得多。我发现这个结果令人双重惊讶:UTF8Encoding源代码如此之快(大量快速路径优化),而且通用 SIMD 模拟代码能够匹配专门调整的代码。

\n
附录:
在上面的代码中,我提到了使用“new String(Char,int)”构造函数分配目标字符串可能带来的 O(*n*) 性能损失(与过度重新归零相关)。为了完整起见,这里有一个备用入口点,它可以通过将扩展的数据作为“char[]”返回来避免该问题:\n
/// <summary>\n/// \'Widen\' each byte in \'bytes\' to 16-bits with no consideration for\n/// character mapping or encoding\n/// </summary>\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\npublic static unsafe char[] WidenByteArray(byte[] bytes)\n{\n    var rgch = new char[bytes.Length];\n    if (rgch.Length > 0)\n        fixed (char* dst = rgch)\n        fixed (byte* src = bytes)\n            widen_bytes_simd(dst, src, rgch.Length);\n    return rgch;\n}\n
Run Code Online (Sandbox Code Playgroud)\n