Самый быстрый способ чтения и записи двоичного файла - PullRequest
11 голосов
/ 10 января 2010

В настоящее время я оптимизирую приложение, одна из операций, которая выполняется очень часто, - чтение и запись двоичного файла. Мне нужно 2 типа функций:

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

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

Эти функции необходимы для подписанных и беззнаковых коротких, целых и длинных в порядке старшего и младшего байтов.

Вот несколько примеров, которые я привел, но мне нужна оценка преимуществ и недостатков:

первый метод использует Marshal для записи значения в память байта [], второй использует простые указатели для достижения этой цели, а третий использует 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);
}

А вот методы чтения / получения:

первый использует Marshal для чтения значения из байта [], второй использует простые указатели, а третий снова использует 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);
}

необходимо проверить границы, но это еще не часть моего вопроса ...

Я был бы рад, если бы кто-то мог сказать, что было бы лучшим и быстрым способом в этом случае, или дать мне некоторые другие решения для работы. Общее решение было бы предпочтительным


Я только что провел тестирование производительности, вот результаты:

Установить маршала: 45 мс, Установить указатель: 48 мс, Установить BitConverter: 71 мс Получить маршал: 45 мс, Получить указатель: 26 мс, Получить BitConverter: 30 мс

кажется, что использование указателей - быстрый способ, но я думаю, что Marshal и BitConverter проводят внутреннюю проверку ... может кто-нибудь это проверить?

Ответы [ 4 ]

10 голосов
/ 10 января 2010

Важно: если вам нужен только один порядковый номер, посмотрите магию указателя с помощью wj32 / dtb


Лично я бы писал прямо в Stream (возможно, с некоторой буферизацией) и повторно использовал общий буфер, который, как я могу вообще предположить, чистый. Затем вы можете сделать несколько ярлыков и принять индекс 0/1/2 / 3.

Конечно, не используйте BitConverter, так как это не может быть использовано как для little / big-endian, что вам требуется. Я также был бы склонен просто использовать сдвиг битов, а не небезопасный и т. Д. Он на самом деле самый быстрый, основываясь на следующем (поэтому я рад, что так я и делаю, мой код здесь искать 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);
    }
}
8 голосов
/ 10 января 2010

Используя от Set1 до Set4 Марка Гравелла и 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");
3 голосов
/ 10 января 2010

Указатели - это путь. Закрепление объектов с помощью ключевого слова fixed чрезвычайно дешево, и вы избегаете накладных расходов на вызов таких функций, как WriteInt32 и BlockCopy. Для «общего решения» вы можете просто использовать void * и использовать свой собственный memcpy (поскольку вы имеете дело с небольшими объемами данных). Однако указатели не работают с истинными обобщениями.

1 голос
/ 10 января 2010

Вы должны выполнить некоторое профилирование своего кода, чтобы определить, является ли это узким местом. Кроме того, при просмотре вашего кода выясняется, что вы используете вызовы функций .Net для записи одного байта в неуправляемый массив, включая вывод в память и вызов небезопасного кода ...

Возможно, вам гораздо лучше объявить .Net System.IO.MemoryStream и искать и переписываться по нему, где это возможно, используя потоковую запись, чтобы проталкивать ваши изменения, которая должна использовать меньше вызовов функций и не требует небезопасных код. Вы найдете полезность указателя в C #, если вы делаете такие вещи, как DSP, где вам нужно выполнить одну операцию для каждого значения в массиве и т. Д.

EDIT: Позвольте мне также упомянуть, что в зависимости от того, что вы делаете, вы можете обнаружить, что кеширование процессора вступит в силу, если вы сможете продолжать работать с одной небольшой областью памяти, которая помещается в кеш, то вы получите лучшую производительность.

...