Самый быстрый способ чтения и записи двоичных файлов

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

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

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

Эти функции необходимы для подписанных и unsigned short, int и long в больших и маленьких порядках.

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

первый метод использует маршал для записи значения в память байта [], второй использует простые указатели для выполнения этого, а третий использует 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:

первый использует маршал для чтения значения из байта [], второй использует простые указатели, а третий использует битконвертер снова:

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 мс, установить битконвертер: 71 мс Get Marshal: 45 ms, Get Pointer: 26 ms, Get BitConverter: 30 мс

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

Ответ 1

Важно:, если вам нужен только один конец, см. волшебство указателя wj32/dtb


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

Разумеется, не используйте BitConverter, поскольку это не может использоваться как для маленьких, так и для больших чисел, которые вам нужны. Я также был бы склонен просто использовать бит-сдвиг, а не небезопасный и т.д. Он является самым быстрым на основе следующего (так что я рад, что так я уже делаю это свой код здесь, найдите 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);
    }
}

Ответ 2

Используя Marc Gravell Set1 to 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

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

Ответ 4

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

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

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