【问题标题】:How to zero out memory allocated by Marshal.AllocHGlobal?如何将 Marshal.AllocHGlobal 分配的内存清零?
【发布时间】:2010-12-02 00:01:43
【问题描述】:

我正在通过Marshal.AllocHGlobal 在我的应用程序中分配一些非托管内存。然后,我将一组字节复制到该位置并将生成的内存段转换为struct,然后通过Marshal.FreeHGlobal 再次释放内存。

方法如下:

public static T Deserialize<T>(byte[] messageBytes, int start, int length)
    where T : struct
{
    if (start + length > messageBytes.Length)
        throw new ArgumentOutOfRangeException();

    int typeSize = Marshal.SizeOf(typeof(T));
    int bytesToCopy = Math.Min(typeSize, length);

    IntPtr targetBytes = Marshal.AllocHGlobal(typeSize);
    Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy);

    if (length < typeSize)
    {
        // Zero out additional bytes at the end of the struct
    }

    T item = (T)Marshal.PtrToStructure(targetBytes, typeof(T));
    Marshal.FreeHGlobal(targetBytes);
    return item;
}

这在大多数情况下都有效,但是如果我的字节数少于struct 所需的大小,则将“随机”值分配给最后一个字段(我在目标结构上使用LayoutKind.Sequential)。我想尽可能高效地将这些悬挂字段归零。

就上下文而言,此代码正在反序列化从 Linux 上的 C++ 发送的高频多播消息。

这是一个失败的测试用例:

// Give only one byte, which is too few for the struct
var s3 = MessageSerializer.Deserialize<S3>(new[] { (byte)0x21 });
Assert.AreEqual(0x21, s3.Byte);
Assert.AreEqual(0x0000, s3.Int); // hanging field should be zero, but isn't

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct S3
{
    public byte Byte;
    public int Int;
}

重复运行此测试会导致第二个断言每次失败并返回不同的值。


编辑

最后,我使用leppie's suggestionunsafe 和使用stackalloc。这分配了一个根据需要清零的字节数组,并将吞吐量从 50% 提高到 100%,具体取决于消息大小(更大的消息会带来更大的好处)。

最终的方法类似于:

public static T Deserialize<T>(byte[] messageBytes, int startIndex, int length)
    where T : struct
{
    if (length <= 0)
        throw new ArgumentOutOfRangeException("length", length, "Must be greater than zero.");
    if (startIndex < 0)
        throw new ArgumentOutOfRangeException("startIndex", startIndex, "Must be greater than or equal to zero.");
    if (startIndex + length > messageBytes.Length)
        throw new ArgumentOutOfRangeException("length", length, "startIndex + length must be <= messageBytes.Length");

    int typeSize = Marshal.SizeOf(typeof(T));
    unsafe
    {
        byte* basePtr = stackalloc byte[typeSize];
        byte* b = basePtr;
        int end = startIndex + Math.Min(length, typeSize);
        for (int srcPos = startIndex; srcPos < end; srcPos++)
            *b++ = messageBytes[srcPos];
        return (T)Marshal.PtrToStructure(new IntPtr(basePtr), typeof(T));
    }   
}

不幸的是,这仍然需要调用 Marshal.PtrToStructure 来将字节转换为目标类型。

【问题讨论】:

    标签: .net memory unmanaged-memory


    【解决方案1】:

    我以前从未在 C# 中做过这些事情,但我在 MSDN 中找到了 Marshal.WriteByte(IntPtr, Int32, Byte)。试试看。

    【讨论】:

      【解决方案2】:

      为什么不只检查start + length 是否在typesize 内?

      顺便说一句:我会在这里使用unsafe 并使用 for 循环将额外的内存归零。

      这也将使您受益于使用stackalloc,它比AllocGlobal 更安全、更快捷。

      【讨论】:

      • @leppie -- 感谢您提供有用的信息。我也会查看stackalloc。我必须迎合不同的消息大小,因为如果我们在另一端忽略的一端添加字段,两个团队偶尔可以设法避免同步发布。同样,如果您不需要值,则可以期望它们并取为零,这就是我在这里尝试实现的情况。
      • @leppie,我倾向于这种方法。您能否详细说明为什么使用stackalloc 更安全、更快捷?一旦我有了byte*,复制到它的最佳方法是什么?
      • 我已经整理了一个与stackalloc 一起使用的版本来填充堆栈上的数组。不过,我认为不可能绕过拨打Marshal.PtrToStructure 的电话,是吗?
      • @Drew:不,我也没有意识到泛型在变得不安全时会如此糟糕:(如果你的类型是已知的,你可以生成所有的“模板”。这样可以保持速度。
      • 不幸的是,这是一个通用 API,可以处理未知和不同的类型(尽管所有类型都是固定大小的)。
      【解决方案3】:

      是的,正如Jon Seigel 所说,您可以使用 Marshal.WriteByte 将其归零

      在以下示例中,我在复制结构之前将缓冲区清零。

      if (start + length > messageBytes.Length) 
          throw new ArgumentOutOfRangeException();   
      int typeSize = Marshal.SizeOf(typeof(T));    
      int bytesToCopy = Math.Min(typeSize, length);   
      IntPtr targetBytes = Marshal.AllocHGlobal(typeSize);  
      //zero out buffer
      for(int i=0; i < typeSize; i++)
      {
          Marshal.WriteByte(targetBytes, i, 0);
      }
      Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy); 
      

      【讨论】:

      • 对 Marshal.WriteByte 的每次调用都会导致托管代码和本机代码之间来回转换,这有一定的开销。在循环中这样做可能会效率低下。如果你想坚持 Marshal 类,我会试试这个:Marshal.Copy(new byte[typeSize], 0, targetBytes, typeSize)
      • 我想到的另一个选择是 P/Invoke LocalAlloc 函数并传入 LPTR 标志。
      【解决方案4】:
      [DllImport("kernel32.dll")]
      static extern void RtlZeroMemory(IntPtr dst, UIntPtr length);
      ...
      RtlZeroMemory(targetBytes, typeSize);
      

      【讨论】:

      • kernel32.dll 上的 Dumpbin.exe 说它不仅仅是一个宏。
      • @MattiasS -- 我需要在dst + N 处归零。 IntPtr 不支持算术,那么我该如何解决这个偏移量?
      • 您不能在 Marshal.Copy 调用之前简单地将整个缓冲区清零吗?这样,您不使用结构覆盖的任何部分都将保持为零。如果将指针值转换为 long 然后返回 IntPtr,则可以对指针值进行算术运算。
      • 我认为第二个参数应该是IntPtr而不是int
      • 这是唯一正确的答案。没有理由使用其他任何东西。在 windows.h 中,这是一个仅用于代码互操作性的宏。 NtosKrnl 版本声称是特定于平台的,请参阅msdn.microsoft.com/en-us/library/windows/hardware/…
      【解决方案5】:

      这在 Windows 上可以正常工作:

      namespace KernelPInvoke
      {
          /// <summary>
          /// Implements some of the C functions declared in string.h
          /// </summary>
          public static class MemoryWrapper
          {
              [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
              static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);
      
              [DllImport("kernel32.dll", EntryPoint = "MoveMemory", SetLastError = false)]
              static extern void MoveMemory(IntPtr destination, IntPtr source, uint length);
      
              [DllImport("kernel32.dll", EntryPoint = "RtlFillMemory", SetLastError = false)]
              static extern void FillMemory(IntPtr destination, uint length, byte fill);
          }
      
          var ptr = Marshal.AllocHGlobal(size);
          try
          {
              MemoryWrapper.FillMemory(ptr, size, 0);
              // further work...
          }
          finally
          {
              Marshal.FreeHGlobal(ptr);
          }
      }
      

      【讨论】:

        【解决方案6】:

        我认为将缓冲区归零的最佳方法是,如果您不想要,或者不能采用其他方式:

        for(int i=0; i<buffSize; i++)
        {
            Marshal.WriteByte(buffer, i, 0x00);
        }
        

        【讨论】:

          【解决方案7】:
          for(int i=0; i < buffSize / 8; i += 8 )
          {
              Marshal.WriteInt64(buffer, i, 0x00);
          }
          
          for(int i= buffSize % 8 ; i < -1 ; i-- )
          {
              Marshal.WriteByte (buffer, buffSize - i, 0x00);
          }
          

          我认为您会发现使用 64 位写入而不是 8 位写入(最后几个字节仍然需要)快几倍。

          【讨论】:

            【解决方案8】:

            如果您使用的是 Net Core 或 NET5,现在可以致电 Unsafe.InitBlockUnaligned

            Unsafe.InitBlockUnaligned((byte*)ptr, 0, byteCount) 
            

            对于不重要的数据大小,这比手动执行指针循环要快一个数量级,因为它使用平台特定的内在来实现完全硬件加速。您可以从 kernel32 解决方案中受益,但可以跨平台,无需手动管理原生依赖项。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-12-26
              • 2015-08-21
              • 2010-12-09
              • 2015-08-21
              相关资源
              最近更新 更多