.net - Cómo poner a cero la memoria asignada por Marshal.AllocHGlobal?
memory unmanaged-memory (7)
Estoy asignando memoria no administrada en mi aplicación a través de Marshal.AllocHGlobal
. Luego copio un conjunto de bytes en esta ubicación y convierto el segmento de memoria resultante en una struct
antes de liberar la memoria nuevamente a través de Marshal.FreeHGlobal
.
Este es el método:
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;
}
Esto funciona en su mayor parte, sin embargo, si tengo menos bytes que el tamaño de la struct
, entonces los valores ''aleatorios'' se asignan a los últimos campos (estoy usando LayoutKind.Sequential
en la estructura de destino). Me gustaría poner a cero estos campos colgantes de la manera más eficiente posible.
Por contexto, este código está deserializando mensajes de multidifusión de alta frecuencia enviados desde C ++ en Linux.
Aquí hay un caso de prueba fallido:
// 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;
}
Ejecutar esta prueba repetidamente hace que la segunda afirmación falle con un valor diferente cada vez.
EDITAR
Al final, utilicé la sugerencia de stackalloc
unsafe
ser unsafe
y usar stackalloc
. Esto asignó una matriz de bytes que se puso a cero según sea necesario, y un rendimiento mejorado de entre 50% y 100%, dependiendo del tamaño del mensaje (los mensajes más grandes tuvieron un mayor beneficio).
El método final terminó pareciéndose a:
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));
}
}
Lamentablemente, esto todavía requiere una llamada a Marshal.PtrToStructure
para convertir los bytes en el tipo de destino.
¿Por qué no solo comprobar si start + length
está dentro de typesize
?
Por cierto: me gustaría ir unsafe
aquí y utilizar un bucle for para poner a cero la memoria adicional.
Eso también te dará el beneficio de usar stackalloc
que es mucho más seguro y más rápido que AllocGlobal
.
Creo que la mejor manera de poner a cero un búfer es esto, si no quieres, o no puedes ir por el otro camino:
for(int i=0; i<buffSize; i++)
{
Marshal.WriteByte(buffer, i, 0x00);
}
Esto funcionará bien en 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);
}
}
Nunca antes había hecho esto en C #, pero encontré Marshal.WriteByte (IntPtr, Int32, Byte) en MSDN. Pruébalo.
Sí, como dijo Jon Seigel , puedes ponerlo en cero usando Marshal.WriteByte
En el siguiente ejemplo, pongo a cero el búfer antes de copiar la estructura.
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);
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);
}
Creo que lo encontrará varias veces más rápido usando wrights de 64 bits en lugar de wrights de 8 bits (que aún necesita para los últimos bytes).
[DllImport("kernel32.dll")]
static extern void RtlZeroMemory(IntPtr dst, int length);
...
RtlZeroMemory(targetBytes, typeSize);