Unsafe cast’инг в .Net

Решил я тут на днях позаниматься немного программизмом, причем не для работы, а так, для себя, точнее для своего телефона ;). Стояла такая задача — загрузить из бинарного файла массив float’ов, причем сделать это максимально эффективно в плане быстродействия (под телефон же все-таки).

Оказалось, что в дотнете сделать это не так-то просто. Есть 2 пути — первое — это использовать BinaryReader и в цикле считывать из файла числа, и второй — прочитать весь файл в массив байт (byte[]) и потом из него копировать во float[]. И оба этих варианта меня понятное дело не устроили — первый потому что это медленно (насколько именно я не замерял конечно, но даже на вызовы функций уйдет время довольно приличное, если большое количество чисел надо прочитать), а второй — потому что это займет двойной объем памяти — сначала под массив байт, потом под массив float’ов.

Идеальным вариантом было бы конечно прочитать сразу все данные файла в массив байт и потом просто привести его к float[], тогда и прочитаем быстро и памяти лишней не сожрем. Но в дотнете этого сделать нельзя!

var file = new FileStream("myfile.bin", FileMode.Open);
byte[] data = new byte[40000];
file.Read(data, 0, 40000);
float[] numbers = (float[])data; // Вот так было бы очень здорово конечно, но к сожалению нельзя... :(

Очень расстроенный я полез искать решение в Интернетах, и обнаружил что я не один такой -)))) На многих форумах задавался тот же вопрос и на него не было ответа, и лишь когда я уже совсем потерял надежду и начал рвать на жопе волосы подумывать переписать все на сиплюплюсе, то наткнулся на гениальное решение одного омерекоса. Он предложил нае.. обмануть дотнет следующим образом: использовать класс, который использует явный StructLayout для своих членов, иными словами:

        
[StructLayout(LayoutKind.Explicit)]
private class FloatBuffer
{
     [FieldOffset(0)]
     public byte[] bytes;
     [FieldOffset(0)]
     public float[] floats;
}

То есть класс содержит массив байт и массив float’ов, но они находятся в одном и том же месте. То есть мы считываем массив байт из файла, а потом можем просто обращаться к нему как массиву float. Урра, товарищи! Теперь как использовать это в коде:

public class Test
{
        [StructLayout(LayoutKind.Explicit)]
        private class FloatBuffer
        {
            [FieldOffset(0)]
            public byte[] bytes;
            [FieldOffset(0)]
            public float[] floats;
        }

        public void LoadFromFile(String fileName)
        {
           var file = new FileStream(fileName, FileMode.Open);
           FloatBuffer buffer = new FloatBuffer();
           buffer.bytes = new byte[file.Length];
           file.Read(buffer.bytes, 0, file.Length);
           float[] test = buffer.floats; // Вуаля!
        }
}

Но здесь есть несколько моментов, о которых нельзя забывать. Так как массивы используют одну и ту же область памяти, то

1) в режиме отладки просмотр этих значений невозможен и Watch для них некорректно работает (студия слегка сходит с ума), то есть если поставить брейкпойнт и навести мышь на buffer.floats, то он покажет что это массив байт -)))). Но во время выполнения программа работает корректно.

2) неправильно определяется длина массива. Если например сделать buffer.bytes = new byte[4000], то buffer.floats.Length тоже будет 4000, хотя на самом деле их всего 1000, об этом нужно помнить и не пытаться читать писать за пределами массива. Насколько я понял — длина массива показывается в элементах того массива, который был первый создан

3) (это только догадка, проверять мне было лень:) Если проинициализировать оба массива, то возможно будет утечка памяти -))))

Ну и естественно эти преобразования работают только для value-типов по вполне понятным причинам.

Вот и все, теперь вы можете делать любые небезопасные касты для value-типов, удачного вам разрушения heap’a! 😉

Рейтинг
( Пока оценок нет )
Загрузка ...