最快的二进制读写方式

时间:2010-01-10 10:36:05

标签: c# file-io binary

我目前正在优化一个应用程序,其中一个经常执行的操作是读写二进制文件。我需要两种类型的功能:

Set(byte[] target, int index, int value);

int Get(byte[] source, int index);

这些函数是有符号和无符号short,int和long的大端和小端序。

以下是我所做的一些例子,但我需要对优缺点进行评估:

第一种方法是使用Marshal将值写入byte []的内存中,第二种方法是使用普通指针来完成此操作,第三种方法使用BitConverter和BlockCopy来执行此操作

unsafe void Set(byte[] target, int index, int value)
{
    fixed (byte* p = &target[0])
    {
        Marshal.WriteInt32(new IntPtr(p), index, value);
    }
}

unsafe void Set(byte[] target, int index, int value)
{
    int* p = &value;
    for (int i = 0; i < 4; i++)
    {
        target[offset + i] = *((byte*)p + i);
    }
}

void Set(byte[] target, int index, int value)
{
    byte[] data = BitConverter.GetBytes(value);
    Buffer.BlockCopy(data, 0, target, index, data.Length);
}

以下是Read / Get方法:

第一个是使用Marshal从byte []读取值,第二个是使用普通指针,第三个是再次使用BitConverter:

unsafe int Get(byte[] source, int index)
{
    fixed (byte* p = &source[0])
    {
        return Marshal.ReadInt32(new IntPtr(p), index);
    }
}

unsafe int Get(byte[] source, int index)
{
    fixed (byte* p = &source[0])
    {
        return *(int*)(p + index);
    }
}

unsafe int Get(byte[] source, int index)
{
    return BitConverter.ToInt32(source, index);
}

边界检查需要完成,但不是我的问题的一部分......

如果有人能说出在这种情况下最好和最快的方法,或者给我一些其他解决方案,我会很高兴。通用的解决方案更可取


我刚做了一些性能测试,结果如下:

设置Marshal:45 ms,设置指针:48 ms,设置BitConverter:71 ms 获得Marshal:45 ms,获取指针:26 ms,获取BitConverter:30 ms

似乎使用指针是快速的方法,但我认为Marshal和BitConverter会进行一些内部检查......有人可以验证这个吗?

4 个答案:

答案 0 :(得分:10)

重要提示:如果您只需要一个端点,请参阅wj32 / dtb的指针魔力


就个人而言,我会直接写一个Stream(也许有一些缓冲),并重新使用我通常认为是干净的共享缓冲区。然后你可以做一些快捷方式并假设索引0/1/2/3。

当然不要使用BitConverter,因为它不能用于您需要的little / big-endian。我也倾向于使用位移而不是不安全等。基于以下内容,它实际上是最快的(所以我很高兴我已经这样做了我的代码here,寻找EncodeInt32Fixed):

Set1: 371ms
Set2: 171ms
Set3: 993ms
Set4: 91ms <==== bit-shifting ;-p

代码:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
static class Program
{
    static void Main()
    {
        const int LOOP = 10000000, INDEX = 100, VALUE = 512;
        byte[] buffer = new byte[1024];
        Stopwatch watch;

        watch = Stopwatch.StartNew();
        for (int i = 0; i < LOOP; i++)
        {
            Set1(buffer, INDEX, VALUE);
        }
        watch.Stop();
        Console.WriteLine("Set1: " + watch.ElapsedMilliseconds + "ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < LOOP; i++)
        {
            Set2(buffer, INDEX, VALUE);
        }
        watch.Stop();
        Console.WriteLine("Set2: " + watch.ElapsedMilliseconds + "ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < LOOP; i++)
        {
            Set3(buffer, INDEX, VALUE);
        }
        watch.Stop();
        Console.WriteLine("Set3: " + watch.ElapsedMilliseconds + "ms");

        watch = Stopwatch.StartNew();
        for (int i = 0; i < LOOP; i++)
        {
            Set4(buffer, INDEX, VALUE);
        }
        watch.Stop();
        Console.WriteLine("Set4: " + watch.ElapsedMilliseconds + "ms");

        Console.WriteLine("done");
        Console.ReadLine();
    }
    unsafe static void Set1(byte[] target, int index, int value)
    {
        fixed (byte* p = &target[0])
        {
            Marshal.WriteInt32(new IntPtr(p), index, value);
        }
    }

    unsafe static void Set2(byte[] target, int index, int value)
    {
        int* p = &value;
        for (int i = 0; i < 4; i++)
        {
            target[index + i] = *((byte*)p + i);
        }
    }

    static void Set3(byte[] target, int index, int value)
    {
        byte[] data = BitConverter.GetBytes(value);
        Buffer.BlockCopy(data, 0, target, index, data.Length);
    }
    static void Set4(byte[] target, int index, int value)
    {
        target[index++] = (byte)value;
        target[index++] = (byte)(value >> 8);
        target[index++] = (byte)(value >> 16);
        target[index] = (byte)(value >> 24);
    }
}

答案 1 :(得分:8)

使用Marc Gravell的Set1Set4和下面的Set5,我的机器上会显示以下数字:

Set1: 197ms
Set2: 102ms
Set3: 604ms
Set4: 68ms
Set5: 55ms <==== pointer magic ;-p

代码:

unsafe static void Set5(byte[] target, int index, int value)
{
    fixed (byte* p = &target[index])
    {
        *((int*)p) = value;                
    }
}

当然,当字节数组在每次迭代时没有固定但只有一次时,它会更快

Set6: 10ms (little endian)
Set7: 85ms (big endian)

代码:

if (!BitConverter.IsLittleEndian)
{
    throw new NotSupportedException();
}

watch = Stopwatch.StartNew();
fixed (byte* p = buffer)
{
    for (int i = 0; i < LOOP; i++)
    {
        *((int*)(p + INDEX)) = VALUE;
    }
}
watch.Stop();
Console.WriteLine("Set6: " + watch.ElapsedMilliseconds + "ms");

watch = Stopwatch.StartNew();
fixed (byte* p = buffer)
{
    for (int i = 0; i < LOOP; i++)
    {
        *((int*)(p + INDEX)) = System.Net.IPAddress.HostToNetworkOrder(VALUE);
    }
}
watch.Stop();
Console.WriteLine("Set7: " + watch.ElapsedMilliseconds + "ms");

答案 2 :(得分:3)

指针是要走的路。使用 fixed 关键字固定对象非常便宜,并且可以避免调用WriteInt32和BlockCopy等函数的开销。对于“通用解决方案”,您可以简单地使用void *并使用您自己的memcpy(因为您处理的是少量数据)。但是指针不适用于真正的泛型。

答案 3 :(得分:1)

您应该对代码进行一些分析,以揭示这是否是瓶颈。另外看一下你的代码,看来你正在使用.Net函数调用将一个字节写入一个非托管数组,包括内存上的一个引脚和一个不安全代码的调用......

你可能会更好地宣布一个.Net System.IO.MemoryStream并寻求和写入它,尽可能使用流编写器推送你的更改,这应该使用更少的函数调用,并且不需要不安全码。你会发现指针在C#中更有用,如果你正在做像DSP这样的事情,你需要对数组中的每个值执行单个操作等。

编辑: 我还要提一下,根据您正在做的事情,您可能会发现CPU缓存将生效,如果您可以继续处理适合缓存的单个小区域内存,那么您将获得最佳性能。