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

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

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

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

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

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

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

И вот Читать/Получать методы:

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

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

11
задан tshepang 26 August 2014 в 20:26
поделиться

4 ответа

Важно: если вам нужен только один эндиан, см. магию указателей wj32 / dtb


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

Конечно, не используйте BitConverter, так как это не может быть использовано для обоих little/big-endian, который вам нужен. Я также был бы склонен просто использовать битовое смещение, а не опасное и т.д. В действительности он самый быстрый, основываясь на следующем (так что я рад, что так я уже делаю мой код здесь , ищите EncodeInt32Fixed):

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

code:

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);
    }
}
10
ответ дан 3 December 2019 в 05:12
поделиться

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

3
ответ дан 3 December 2019 в 05:12
поделиться

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

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

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

1
ответ дан 3 December 2019 в 05:12
поделиться

Используя настройки Марка Грэвелла Set1 - Set4 и Set5 ниже, я получаю на своем аппарате следующие цифры:

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

Code:

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");
8
ответ дан 3 December 2019 в 05:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: