代码之家  ›  专栏  ›  技术社区  ›  Nosrama

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

  •  7
  • Nosrama  · 技术社区  · 17 年前

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

    private static readonly Encoding encoding = new ASCIIEncoding();
    //...
    byte[] bytes = encoding.GetBytes(someString);
    socket.Send(bytes);
    //...
    
    9 回复  |  直到 8 年前
        1
  •  16
  •   Jon Skeet    17 年前

    如果你所有的数据都是 如果是ASCII码,那么您可以比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);
        }
    }
    

    然后你会做一些类似的事情:

    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);
    

    不过,这是相当绝望的优化。我会坚持 直到我离开 经过证实的 这是一个瓶颈(或者至少这种粗鄙的黑客没有帮助)。

        2
  •  9
  •   jrista    17 年前

    以下是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;
    }
    
        3
  •  4
  •   Glenn Slayden    8 年前



    - Intel 64 and IA-32 Architectures Optimization Reference Manual (2018年4月)§3.7.6.1


    byte[] 和“宽”(16位,Unicode)文本,您将要考虑部署的解决方案。 SIMD 说明书 PUNPCKLBW + PUNPCKHBW PACKUSWB (缩小)。在里面 .NET ,它们是新推出的x64 JIT内部函数,用于硬件加速 System.Numerics Vector 和 Vector<T> (见 here 向量<T> 定义在 System.Numerics.Vectors 该软件包目前仍处于相当积极的开发中。如下图所示,您可能还希望包括 System.Runtime.CompilerServices.Unsafe 软件包,因为这是制造商推荐的首选SIMD加载/存储技术 向量<T>

    相关的单指令多数据加速功能仅对中的有能力CPU启用 x64模式 ,但除此之外,.NET在 做 事实上,在更广泛的.NET生态系统中可靠地运行,可能会降低性能。为了测试下面显示的代码,我使用了一个完整的控制台应用程序 .NET Framework 4.7.2 x86 (模拟的)模式。

    Vector.Widen 举例说明 字节[] char[] C#7 . 在本例中,执行相反的操作-- 即 Vector.Narrow 实现缩小方向——非常简单,留给读者作为练习。

    警告:
    这里建议的方法完全没有编码意识,它们只是将原始字节剥离/扩展或缩小/加宽到原始字节,而不考虑字符映射、文本编码或其他语言属性。加宽时,多余字节设置为零,缩小时,多余字节被丢弃。

    其他人也讨论了这个问题 众多危险 与本页和其他地方的实践相关联,因此请仔细阅读 review and understand 在考虑是否适合您的情况之前,请先了解此操作的性质。为清楚起见,以下代码示例中省略了内联验证,但可以将其添加到最内部的循环中,对SIMD的好处影响最小。

    . 虽然不是SIMD加速,但使用合适的 Encoding 实例 . 尽管OP确实明确要求最大的表现(或者更明显地说,作为防止轻蔑警察投反对票的最后一搏),接下来我适当地总结了正常情况下应该使用的适当的、认可的技术。

    将字节数组扩展到.NET的步骤 String ,调用 GetString() 在适当的面向字节的编码实例上的方法:

    String Encoding.ASCII.GetString(byte[] bytes)
    

    对于(例如,Ascii)字节数组,调用 GetBytes()

    byte[] Encoding.ASCII.GetBytes(char[] chars)
    


    C# 字节数组“哑”加宽的代码。作为提醒,以下是一些应参考的依赖项:

    // ... 
    using System.Numerics;                  // nuget: System.Numerics.Vectors
    using System.Runtime.CompilerServices;  // nuget: System.Runtime.CompilerServices.Unsafe
    // ... 
    

    字符[] 而不是

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

    接下来是主工作循环体。请注意序言循环,它将目标与16字节内存边界对齐,如果需要,可以通过字节复制最多15个源字节。这确保了主管道的最有效运行。” 四边形 “使用单对SIMD进行循环 PUNPCKLBW/PUNPCKHBW 指令,一次写入32字节(获取16个源字节,然后存储为占用32字节的16个宽字符)。预对齐,加上 dst )是上面引用的英特尔手册中的官方建议。同样,对齐操作要求当主循环完成时,源可能有多达15个剩余的尾随字节;这些都是结束了一个简短的尾声循环。

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

    事实上,这就是全部!它就像一个符咒,正如你在下面看到的,它确实“尖叫” 如广告所示

    JIT为“发布”构建生成 .NET 4.7.2

    L_4223  mov         rax,rbx  
    L_4226  movups      xmm0,xmmword ptr [rax] ; fetch 16 bytes
    L_4229  mov         rax,rdi  
    L_422C  lea         rdx,[rdi+10h]  
    L_4230  movaps      xmm2,xmm0  
    L_4233  pxor        xmm1,xmm1  
    L_4237  punpcklbw   xmm2,xmm1               ; interleave 8-to-16 bits (lo)
    L_423B  movups      xmmword ptr [rax],xmm2  ; store 8 bytes (lo) to 8 wide chars (16 bytes)
    L_423E  pxor        xmm1,xmm1  
    L_4242  punpckhbw   xmm0,xmm1               ; interleave 8-to-16 bits (hi)
    L_4246  movups      xmmword ptr [rdx],xmm0  ; store 8 bytes (hi) to 8 wide chars (16 bytes)
    L_4249  add         rbx,10h  
    L_424D  add         rdi,20h  
    L_4251  add         esi,0FFFFFFF0h  
    L_4254  test        esi,esi  
    L_4256  jge         L_4223  
    L_4258  ...
    

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

    • ASCII的.NET编码
    • UTF-8的.NET编码(无BOM,无投掷)

    试验包括所有试验装置的相同工作和所有试验装置相同结果的验证。测试字节是随机的,仅限ASCII码( [0x01 - 0x7F] )以确保所有试验装置的结果相同。输入大小是随机的,最大1MB,带有日志 偏小尺寸,平均尺寸约为80K。

                     Relative ticks, normalized to best result
                      .NET Framework 4.7.3056.0 x64 (release)
       iter      naive      win-1252         ascii         utf-8          simd
    -------   -----------  ------------  ------------  ------------   -----------
      10000 |    131.5         294.5         186.2         145.6         100.0
      20000 |    137.7         305.3         191.9         149.4         100.0
      30000 |    139.2         308.5         195.8         151.5         100.0
      40000 |    141.8         312.1         198.5         153.2         100.0
      50000 |    142.0         313.8         199.1         154.1         100.0
      60000 |    140.5         310.6         196.7         153.0         100.0
      70000 |    141.1         312.9         197.3         153.6         100.0
      80000 |    141.6         313.7         197.8         154.1         100.0
      90000 |    141.3         313.7         197.9         154.3         100.0
     100000 |    141.1         313.3         196.9         153.7         100.0
    
    gcServer=False; LatencyMode.Interactive; Vector.IsHardwareAccelerated=True 
    

    优先权 x64 平台启用JIT优化且SIMD可用时,没有竞争。SIMD代码的运行速度比下一个竞争者快约150%。这个 Encoding.Default

    前面我提到,测试数据大小的分布强烈地偏向于零。如果没有这一步骤——这意味着大小从0到1048576字节(平均测试大小512K)的均匀分布——SIMD将继续超过软件包,而所有其他单元的性能都相对较好 更糟的

    naive       153.45%
    win-1252    358.84%
    ascii       221.38%
    utf-8       161.62%
    simd        100.00%
    

    至于非SIMD(仿真)情况,UTF-8和SIMD非常接近——通常彼此相差不超过3-4%——并且远远好于其他情况。我发现这一结果令人倍感惊讶:这个 UTF8Encoding source code 是如此之快(大量的快速路径优化),而且通用SIMD仿真代码能够与目的优化代码相匹配。




    增编:

    N )使用 new String(Char,int) 构造函数来分配目标字符串。为了完整起见,这里有一个替代入口点,它可以通过将加宽的数据作为 ushort[] :

    /// <summary>
    /// 'Widen' each byte in 'bytes' to 16-bits with no consideration for
    /// character mapping or encoding
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static unsafe char[] WidenByteArray(byte[] bytes)
    {
        var rgch = new char[bytes.Length];
        if (rgch.Length > 0)
            fixed (char* dst = rgch)
            fixed (byte* src = bytes)
                widen_bytes_simd(dst, src, rgch.Length);
        return rgch;
    }
    
        4
  •  1
  •   Nader Shirazie    17 年前

    你想优化什么?中央处理器?带宽?

    如果要优化带宽,可以尝试事先压缩字符串数据。

    首先,在您尝试在如此低的级别上进行优化之前,分析您的代码,找出慢位是什么。

        5
  •  1
  •   Steve Wortham    17 年前

    我想GetBytes()函数已经为此进行了很好的优化。我想不出任何提高现有代码速度的建议。

    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();
    bf.Serialize(ms, someString);
    byte[] bytes =  ms.ToArray();
    ms.Close();
    socket.Send(bytes);
    

    我之所以这么想 更快的是它跳过了编码步骤。我也不完全确定这是否能正常工作。但是你可以试试看。当然,如果您需要ascii编码,那么这不会有帮助。

    我只是有另一个想法。我相信这段代码返回的字节数是使用ASCII编码的GetBytes的两倍。原因是.NET中的所有字符串都在幕后使用unicode。当然,Unicode每个字符使用2个字节,而ASCII仅使用1个字节。因此,在本例中可能不使用BinaryFormatter,因为通过套接字发送的数据量将增加一倍。

        6
  •  1
  •   Ed Power    17 年前

        7
  •  0
  •   Thomas Levesque    17 年前

    正如其他人所说,编码类已经针对该任务进行了优化,因此可能很难使其更快。你可以做一个微观优化:使用 Encoding.ASCII 而不是 new ASCIIEncoding()

        8
  •  0
  •   kyoryu    17 年前

    我建议你描述一下你在做什么。我发现将字符串转换为字节数组的速度在吞吐量方面比套接字本身的速度更大的问题值得怀疑。

        9
  •  0
  •   user186314 user186314    17 年前

    还有一个提示:我不知道如何创建初始字符串,但请记住StringBuilder.Append(“something”)比myString+=“something”这样的东西要快。

    本