Какой самый быстрый способ преобразовать float[] в байт []?
Я хотел бы получить byte[]
из float[]
настолько быстро, насколько это возможно, без зацикливания всего массива (возможно, посредством приведения). Небезопасный код в порядке. Спасибо!
Я ищу байтовый массив в 4 раза длиннее массива с плавающей точкой (размер байтового массива будет в 4 раза больше размера массива с плавающей запятой, поскольку каждый тип с плавающей запятой состоит из 4 байтов). Я передам это BinaryWriter.
РЕДАКТИРОВАТЬ: Для тех критиков, кричащих "преждевременная оптимизация": я проверил это с помощью профилировщика ANTS, прежде чем оптимизировать. Произошло значительное увеличение скорости, поскольку файл имеет сквозной кэш-память, а массив с плавающей точкой точно соответствует размеру сектора на диске. Двоичный писатель переносит дескриптор файла, созданный с pinvoke
'Win32 API. Оптимизация происходит, поскольку это уменьшает количество вызовов функций.
И, что касается памяти, это приложение создает массивные кэши, которые используют много памяти. Я могу выделить байтовый буфер один раз и повторно использовать его много раз - двойное использование памяти в данном конкретном случае равносильно ошибке округления в общем потреблении памяти приложением.
Так что, думаю, урок здесь не в том, чтобы делать преждевременные предположения;)
9 ответов
Если вы не хотите, чтобы происходило какое-либо преобразование, я бы предложил Buffer.BlockCopy().
public static void BlockCopy(
Array src,
int srcOffset,
Array dst,
int dstOffset,
int count
)
Например:
float[] floatArray = new float[1000];
byte[] byteArray = new byte[floatArray.Length * 4];
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
Есть грязный быстрый (не небезопасный код) способ сделать это:
[StructLayout(LayoutKind.Explicit)]
struct BytetoDoubleConverter
{
[FieldOffset(0)]
public Byte[] Bytes;
[FieldOffset(0)]
public Double[] Doubles;
}
//...
static Double Sum(byte[] data)
{
BytetoDoubleConverter convert = new BytetoDoubleConverter { Bytes = data };
Double result = 0;
for (int i = 0; i < convert.Doubles.Length / sizeof(Double); i++)
{
result += convert.Doubles[i];
}
return result;
}
Это будет работать, но я не уверен в поддержке Mono или более новых версий CLR. Единственное странное, что array.Length
длина байта. Это может быть объяснено, потому что он смотрит на длину массива, хранящегося в массиве, и потому что этот массив был байтовым массивом, длина которого все еще будет в байтовой длине. Индексатор думает, что Double - это восемь байт, поэтому здесь нет необходимости в вычислениях.
Я искал его еще немного, и на самом деле он описан в MSDN: Как: создать объединение C/C++ с использованием атрибутов (C# и Visual Basic), поэтому есть вероятность, что это будет поддерживаться в будущих версиях. Я не уверен насчет Моно, хотя.
Преждевременная оптимизация - корень всего зла! @ Предложение Влада перебирать каждое число с плавающей запятой - гораздо более разумный ответ, чем переключение на байт []. Возьмите следующую таблицу времени выполнения для увеличения количества элементов (в среднем 50 прогонов):
Elements BinaryWriter(float) BinaryWriter(byte[])
-----------------------------------------------------------
10 8.72ms 8.76ms
100 8.94ms 8.82ms
1000 10.32ms 9.06ms
10000 32.56ms 10.34ms
100000 213.28ms 739.90ms
1000000 1955.92ms 10668.56ms
Существует небольшая разница между ними для небольшого количества элементов. Как только вы попадаете в огромное количество элементов, время, потраченное на копирование из float[] в байт [], значительно перевешивает преимущества.
Итак, давайте просто:
float[] data = new float[...];
foreach(float value in data)
{
writer.Write(value);
}
Есть способ избежать копирования и итерации памяти.
Вы можете использовать действительно уродливый хак, чтобы временно изменить массив на другой тип, используя (небезопасные) манипуляции с памятью.
Я протестировал этот хак в 32- и 64-битных ОС, поэтому он должен быть переносимым.
Источник + пример использования поддерживается по адресу https://gist.github.com/1050703, но для вашего удобства я также вставлю его здесь:
public static unsafe class FastArraySerializer
{
[StructLayout(LayoutKind.Explicit)]
private struct Union
{
[FieldOffset(0)] public byte[] bytes;
[FieldOffset(0)] public float[] floats;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct ArrayHeader
{
public UIntPtr type;
public UIntPtr length;
}
private static readonly UIntPtr BYTE_ARRAY_TYPE;
private static readonly UIntPtr FLOAT_ARRAY_TYPE;
static FastArraySerializer()
{
fixed (void* pBytes = new byte[1])
fixed (void* pFloats = new float[1])
{
BYTE_ARRAY_TYPE = getHeader(pBytes)->type;
FLOAT_ARRAY_TYPE = getHeader(pFloats)->type;
}
}
public static void AsByteArray(this float[] floats, Action<byte[]> action)
{
if (floats.handleNullOrEmptyArray(action))
return;
var union = new Union {floats = floats};
union.floats.toByteArray();
try
{
action(union.bytes);
}
finally
{
union.bytes.toFloatArray();
}
}
public static void AsFloatArray(this byte[] bytes, Action<float[]> action)
{
if (bytes.handleNullOrEmptyArray(action))
return;
var union = new Union {bytes = bytes};
union.bytes.toFloatArray();
try
{
action(union.floats);
}
finally
{
union.floats.toByteArray();
}
}
public static bool handleNullOrEmptyArray<TSrc,TDst>(this TSrc[] array, Action<TDst[]> action)
{
if (array == null)
{
action(null);
return true;
}
if (array.Length == 0)
{
action(new TDst[0]);
return true;
}
return false;
}
private static ArrayHeader* getHeader(void* pBytes)
{
return (ArrayHeader*)pBytes - 1;
}
private static void toFloatArray(this byte[] bytes)
{
fixed (void* pArray = bytes)
{
var pHeader = getHeader(pArray);
pHeader->type = FLOAT_ARRAY_TYPE;
pHeader->length = (UIntPtr)(bytes.Length / sizeof(float));
}
}
private static void toByteArray(this float[] floats)
{
fixed(void* pArray = floats)
{
var pHeader = getHeader(pArray);
pHeader->type = BYTE_ARRAY_TYPE;
pHeader->length = (UIntPtr)(floats.Length * sizeof(float));
}
}
}
И использование это:
var floats = new float[] {0, 1, 0, 1};
floats.AsByteArray(bytes =>
{
foreach (var b in bytes)
{
Console.WriteLine(b);
}
});
Вам лучше позволить BinaryWriter сделать это за вас. Будет проходить итерация по всему вашему набору данных независимо от того, какой метод вы используете, так что нет смысла играть с байтами.
Использование нового Span<> в .Net Core 2.1 или более поздней версии...
byte[] byteArray2 = MemoryMarshal.Cast<float, byte>(floatArray).ToArray();
Или, если вместо этого можно использовать Span , то можно выполнить прямое повторное приведение: (очень быстро - нулевое копирование)
Span<byte> byteArray3 = MemoryMarshal.Cast<float, byte>(floatArray);
// with span we can get a byte, set a byte, iterate, and more.
byte someByte = byteSpan[2];
byteSpan[2] = 33;
Я сделал несколько грубых тестов. Время, затраченное на каждую, указано в комментариях. [релиз/без отладчика/x64]
float[] floatArray = new float[100];
for (int i = 0; i < 100; i++) floatArray[i] = i * 7.7777f;
Stopwatch start = Stopwatch.StartNew();
for (int j = 0; j < 100; j++)
{
start.Restart();
for (int k = 0; k < 1000; k++)
{
Span<byte> byteSpan = MemoryMarshal.Cast<float, byte>(floatArray);
}
long timeTaken1 = start.ElapsedTicks; ////// 0 ticks //////
start.Restart();
for (int k = 0; k < 1000; k++)
{
byte[] byteArray2 = MemoryMarshal.Cast<float, byte>(floatArray).ToArray();
}
long timeTaken2 = start.ElapsedTicks; ////// 26 ticks //////
start.Restart();
for (int k = 0; k < 1000; k++)
{
byte[] byteArray = new byte[sizeof(float) * floatArray.Length];
for (int i = 0; i < floatArray.Length; i++)
BitConverter.GetBytes(floatArray[i]).CopyTo(byteArray, i * sizeof(float));
}
long timeTaken3 = start.ElapsedTicks; ////// 1310 ticks //////
start.Restart();
for (int k = 0; k < 1000; k++)
{
byte[] byteArray = new byte[sizeof(float) * floatArray.Length];
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
}
long timeTaken4 = start.ElapsedTicks; ////// 33 ticks //////
start.Restart();
for (int k = 0; k < 1000; k++)
{
byte[] byteArray = new byte[sizeof(float) * floatArray.Length];
MemoryStream memStream = new MemoryStream();
BinaryWriter writer = new BinaryWriter(memStream);
foreach (float value in floatArray)
writer.Write(value);
writer.Close();
}
long timeTaken5 = start.ElapsedTicks; ////// 1080 ticks //////
Console.WriteLine($"{timeTaken1/10,6} {timeTaken2 / 10,6} {timeTaken3 / 10,6} {timeTaken4 / 10,6} {timeTaken5 / 10,6} ");
}
Хотя вы можете получить byte*
использование указателя unsafe
а также fixed
, вы не можете конвертировать byte*
в byte[]
для того, чтобы писатель мог принять его как параметр без выполнения копирования данных. Что вы не хотите делать, так как это удвоит объем памяти и добавит дополнительную итерацию к неизбежной итерации, которую необходимо выполнить, чтобы вывести данные на диск.
Вместо этого вам все же лучше перебирать массив чисел с плавающей точкой и записывать каждый float
автору индивидуально, используя Write(double)
метод. Это все еще будет быстро из-за буферизации внутри писателя. Увидеть sixlettervariables
номера.
У нас есть класс с именем LudicrousSpeedSerialization, и он содержит следующий небезопасный метод:
static public byte[] ConvertFloatsToBytes(float[] data)
{
int n = data.Length;
byte[] ret = new byte[n * sizeof(float)];
if (n == 0) return ret;
unsafe
{
fixed (byte* pByteArray = &ret[0])
{
float* pFloatArray = (float*)pByteArray;
for (int i = 0; i < n; i++)
{
pFloatArray[i] = data[i];
}
}
}
return ret;
}
Хотя это в основном делает цикл for за кулисами, он делает работу в одну строку
byte[] byteArray = floatArray.Select(
f=>System.BitConverter.GetBytes(f)).Aggregate(
(bytes, f) => {List<byte> temp = bytes.ToList(); temp.AddRange(f); return temp.ToArray(); });