如何将 Marshal.AllocHGlobal 分配的内存清零?

发布于 2024-08-06 14:07:24 字数 2828 浏览 6 评论 0原文

我通过 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 的建议不安全并使用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 将字节转换为目标类型。

I am allocating some unmanaged memory in my application via Marshal.AllocHGlobal. I'm then copying a set of bytes to this location and converting the resulting segment of memory to a struct before freeing the memory again via Marshal.FreeHGlobal.

Here's the method:

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

This works for the most part, however if I have fewer bytes than the size of the struct requires, then 'random' values are assigned to the last fields (I am using LayoutKind.Sequential on the target struct). I'd like to zero out these hanging fields as efficiently as possible.

For context, this code is deserializing high-frequency multicast messages sent from C++ on Linux.

Here is a failing test case:

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

Running this test repeatedly causes the second assert to fail with a different value each time.


EDIT

In the end, I used leppie's suggestion of going unsafe and using stackalloc. This allocated a byte array that was zeroed as needed, and improved throughput from between 50% and 100%, depending upon the message size (larger messages saw greater benefit).

The final method ended up resembling:

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

Unfortunately this still requires a call to Marshal.PtrToStructure to convert the bytes into the target type.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

尘世孤行 2024-08-13 14:07:24
[DllImport("kernel32.dll")]
static extern void RtlZeroMemory(IntPtr dst, UIntPtr length);
...
RtlZeroMemory(targetBytes, typeSize);
[DllImport("kernel32.dll")]
static extern void RtlZeroMemory(IntPtr dst, UIntPtr length);
...
RtlZeroMemory(targetBytes, typeSize);
池木 2024-08-13 14:07:24

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

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

对于除微不足道的数据大小以外的任何数据,这比手动执行指针循环要快一个数量级,因为它使用特定于平台的内在函数来实现完整的硬件加速。您可以受益于 kernel32 解决方案,但跨平台且无需手动管理本机依赖项。

If you are on Net Core or NET5, you can now call Unsafe.InitBlockUnaligned:

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

For anything but trivial data sizes, this is order of magnitude faster than doing the pointer loop manually, as it uses platform specific intrinsic for full hardware acceleration. You get benefit of kernel32 solution, but cross platform and without need for managing native dependencies manually.

梦里°也失望 2024-08-13 14:07:24

这在 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);
    }
}

This will work fine on 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);
    }
}
花之痕靓丽 2024-08-13 14:07:24

是的,正如 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); 

Yes as Jon Seigel said, you can zero it out using Marshal.WriteByte

In the following example, I zero out the buffer before copying the 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);  
//zero out buffer
for(int i=0; i < typeSize; i++)
{
    Marshal.WriteByte(targetBytes, i, 0);
}
Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy); 
執念 2024-08-13 14:07:24

为什么不直接检查 start + length 是否在 typesize 之内?

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

这也将为您带来使用 stackalloc 的好处,它比 AllocGlobal 更安全、更快。

Why not just check whether start + length is within typesize?

BTW: I would just go unsafe here and use a for loop to to zero out the additional memory.

That too will give you the benefit of using stackalloc which is much safer and faster than AllocGlobal.

情场扛把子 2024-08-13 14:07:24

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

I've never done this stuff in C# before, but I found Marshal.WriteByte(IntPtr, Int32, Byte) in MSDN. Try that out.

桃气十足 2024-08-13 14:07:24
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 位写入(最后几个字节仍然需要它)会快几倍。

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

I think you will find it to be several times faster using 64 bit writes instead of 8 bit writes (which you still need for the last few bytes).

站稳脚跟 2024-08-13 14:07:24

我认为将缓冲区清零的最佳方法是这样,如果您不想,或者不能走其他路:

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

I think the best way to zero out a buffer is this, if you don't want, or can't go the other way:

for(int i=0; i<buffSize; i++)
{
    Marshal.WriteByte(buffer, i, 0x00);
}
你的他你的她 2024-08-13 14:07:24

另一种选择是,如果内存用于已知的结构类型。
使用 C# 的值类型初始值设定项也可以。

var myType = (MyType*)Marshal.AllocHGlobal(Marshal.SizeOf<MyType>());
*myType = new MyType();// clear memory

Another option is, if the memory is for a known structure type.
Using C#'s value type initializer works too.

var myType = (MyType*)Marshal.AllocHGlobal(Marshal.SizeOf<MyType>());
*myType = new MyType();// clear memory
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文