Мне нужно заполнить byte[]
одним значением отличным от нуля. Как я могу сделать это в С# без цикла через каждый byte
в массиве?
Обновление: Комментарии, похоже, разделили это на два вопроса -
- Есть ли метод Framework для заполнения байта [], который может быть сродни
memset
- Каков наиболее эффективный способ сделать это, когда мы имеем дело с очень большим массивом?
Я полностью согласен с тем, что использование простого цикла прекрасно работает, как указывали Эрик и другие. Суть вопроса заключалась в том, чтобы узнать, могу ли я узнать что-то новое о С#:) Я думаю, что метод Джульетты для параллельной операции должен быть еще быстрее, чем простой цикл.
Ориентиры:
Благодаря Микаэлю Свенсону: http://techmikael.blogspot.com/2009/12/filling-array-with-default-value.html
Оказывается, простой цикл for
- это путь, если вы не хотите использовать небезопасный код.
Извинения за неясность в моем первоначальном посте. Эрик и Марк оба верны в своих комментариях; необходимо иметь более сфокусированные вопросы. Спасибо за все предложения и отзывы.
Ответ 1
Вы можете использовать Enumerable.Repeat
:
byte[] a = Enumerable.Repeat((byte)10, 100).ToArray();
Первый параметр - это элемент, который вы хотите повторить, а второй параметр - это количество раз, чтобы повторить его.
Это нормально для небольших массивов, но вы должны использовать метод looping, если вы имеете дело с очень большими массивами, и производительность является проблемой.
Ответ 2
На самом деле, мало известна операция IL под названием Initblk (английская версия), которая точно что. Таким образом, используйте его как метод, который не требует "небезопасного". Здесь вспомогательный класс:
public static class Util
{
static Util()
{
var dynamicMethod = new DynamicMethod("Memset", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
null, new [] { typeof(IntPtr), typeof(byte), typeof(int) }, typeof(Util), true);
var generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Ldarg_2);
generator.Emit(OpCodes.Initblk);
generator.Emit(OpCodes.Ret);
MemsetDelegate = (Action<IntPtr, byte, int>)dynamicMethod.CreateDelegate(typeof(Action<IntPtr, byte, int>));
}
public static void Memset(byte[] array, byte what, int length)
{
var gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
MemsetDelegate(gcHandle.AddrOfPinnedObject(), what, length);
gcHandle.Free();
}
public static void ForMemset(byte[] array, byte what, int length)
{
for(var i = 0; i < length; i++)
{
array[i] = what;
}
}
private static Action<IntPtr, byte, int> MemsetDelegate;
}
И какова производительность? Вот мой результат для Windows/.NET и Linux/Mono (разные ПК).
Mono/for: 00:00:01.1356610
Mono/initblk: 00:00:00.2385835
.NET/for: 00:00:01.7463579
.NET/initblk: 00:00:00.5953503
Так что стоит подумать. Обратите внимание, что полученный IL не подлежит проверке.
Ответ 3
Немного поздно, но следующий подход может быть хорошим компромиссом, не возвращаясь к небезопасному коду. В основном он инициализирует начало массива с помощью обычного цикла, а затем возвращается к Buffer.BlockCopy()
, который должен быть таким же быстрым, как вы можете получить с помощью управляемого вызова.
public static void MemSet(byte[] array, byte value) {
if (array == null) {
throw new ArgumentNullException("array");
}
const int blockSize = 4096; // bigger may be better to a certain extent
int index = 0;
int length = Math.Min(blockSize, array.Length);
while (index < length) {
array[index++] = value;
}
length = array.Length;
while (index < length) {
Buffer.BlockCopy(array, 0, array, index, Math.Min(blockSize, length-index));
index += blockSize;
}
}
Ответ 4
Основываясь на Ответ Лусеро, вот более быстрая версия. Он будет удваивать количество байтов, скопированных с помощью Buffer.BlockCopy
каждой итерации. Интересно, что он превосходит его в 10 раз при использовании относительно небольших массивов (1000), но разница не такая большая для больших массивов (1000000), но всегда быстрее. Хорошая вещь в том, что он хорошо работает даже с небольшими массивами. Он становится быстрее, чем наивный подход при длине = 100. Для миллионного байтового массива он был в 43 раза быстрее.
(протестировано на Intel i7,.Net 2.0)
public static void MemSet(byte[] array, byte value) {
if (array == null) {
throw new ArgumentNullException("array");
}
int block = 32, index = 0;
int length = Math.Min(block, array.Length);
//Fill the initial array
while (index < length) {
array[index++] = value;
}
length = array.Length;
while (index < length) {
Buffer.BlockCopy(array, 0, array, index, Math.Min(block, length-index));
index += block;
block *= 2;
}
}
Ответ 5
Эта простая реализация использует последовательное удвоение и работает довольно хорошо (примерно в 3-4 раза быстрее, чем наивная версия в соответствии с моими критериями):
public static void Memset<T>(T[] array, T elem)
{
int length = array.Length;
if (length == 0) return;
array[0] = elem;
int count;
for (count = 1; count <= length/2; count*=2)
Array.Copy(array, 0, array, count, count);
Array.Copy(array, 0, array, count, length - count);
}
Изменить: прочитав другие ответы, кажется, я не единственный, кто имеет эту идею. Тем не менее, я оставляю это здесь, так как он немного чище, и он работает наравне с другими.
Ответ 6
Если производительность критическая, вы можете использовать небезопасный код и работать напрямую с указателем на массив.
Другим вариантом может быть импорт memset из msvcrt.dll и использование этого. Тем не менее, накладные расходы от вызова, которые могут быть легко превышены, чем выигрыш в скорости.
Ответ 7
Если производительность абсолютно важна, то Enumerable.Repeat(n, m).ToArray()
будет слишком медленным для ваших нужд. Возможно, вы сможете повысить эффективность работы с помощью PLINQ или Параллельная библиотека задач:
using System.Threading.Tasks;
// ...
byte initialValue = 20;
byte[] data = new byte[size]
Parallel.For(0, size, index => data[index] = initialValue);
Ответ 8
Или используйте способ P/Invoke:
[DllImport("msvcrt.dll",
EntryPoint = "memset",
CallingConvention = CallingConvention.Cdecl,
SetLastError = false)]
public static extern IntPtr MemSet(IntPtr dest, int c, int count);
static void Main(string[] args)
{
byte[] arr = new byte[3];
GCHandle gch = GCHandle.Alloc(arr, GCHandleType.Pinned);
MemSet(gch.AddrOfPinnedObject(), 0x7, arr.Length);
}
Ответ 9
Все ответы записывают только одиночные байты - что делать, если вы хотите заполнить массив байтов словами? Или плавает? Я время от времени нахожу использование этого. Поэтому после того, как несколько раз написав аналогичный код с memset в неэквивалентном виде и придя на эту страницу, чтобы найти хороший код для одиночных байтов, я начал писать метод ниже.
Я думаю, что у PInvoke и С++/CLI есть свои недостатки. И почему у вас нет времени выполнения "PInvoke" для вас в mscorxxx? Array.Copy и Buffer.BlockCopy - это, конечно, собственный код. BlockCopy даже не "безопасен" - вы можете скопировать длинную половину над другой или над DateTime, пока они находятся в массивах.
По крайней мере, я бы не стал писать новый проект на С++ для таких вещей - это пустая трата времени почти наверняка.
Итак, здесь в основном расширенная версия решений, представленных Lucero и TowerOfBricks, которые могут использоваться для longset memset, ints и т.д., а также для одиночных байтов.
public static class MemsetExtensions
{
static void MemsetPrivate(this byte[] buffer, byte[] value, int offset, int length) {
var shift = 0;
for (; shift < 32; shift++)
if (value.Length == 1 << shift)
break;
if (shift == 32 || value.Length != 1 << shift)
throw new ArgumentException(
"The source array must have a length that is a power of two and be shorter than 4GB.", "value");
int remainder;
int count = Math.DivRem(length, value.Length, out remainder);
var si = 0;
var di = offset;
int cx;
if (count < 1)
cx = remainder;
else
cx = value.Length;
Buffer.BlockCopy(value, si, buffer, di, cx);
if (cx == remainder)
return;
var cachetrash = Math.Max(12, shift); // 1 << 12 == 4096
si = di;
di += cx;
var dx = offset + length;
// doubling up to 1 << cachetrash bytes i.e. 2^12 or value.Length whichever is larger
for (var al = shift; al <= cachetrash && di + (cx = 1 << al) < dx; al++) {
Buffer.BlockCopy(buffer, si, buffer, di, cx);
di += cx;
}
// cx bytes as long as it fits
for (; di + cx <= dx; di += cx)
Buffer.BlockCopy(buffer, si, buffer, di, cx);
// tail part if less than cx bytes
if (di < dx)
Buffer.BlockCopy(buffer, si, buffer, di, dx - di);
}
}
После этого вы можете просто добавить короткие методы, чтобы взять тип значения, который вам нужен для memset, и вызвать частный метод, например. просто найдите замену ulong в этом методе:
public static void Memset(this byte[] buffer, ulong value, int offset, int count) {
var sourceArray = BitConverter.GetBytes(value);
MemsetPrivate(buffer, sourceArray, offset, sizeof(ulong) * count);
}
Или сделай глупый и делай это с любым типом структуры (хотя MemsetPrivate выше работает только для структур, которые маршалируются до размера, равного двум):
public static void Memset<T>(this byte[] buffer, T value, int offset, int count) where T : struct {
var size = Marshal.SizeOf<T>();
var ptr = Marshal.AllocHGlobal(size);
var sourceArray = new byte[size];
try {
Marshal.StructureToPtr<T>(value, ptr, false);
Marshal.Copy(ptr, sourceArray, 0, size);
} finally {
Marshal.FreeHGlobal(ptr);
}
MemsetPrivate(buffer, sourceArray, offset, count * size);
}
Я изменил initblk, упомянутый ранее, чтобы взять ulongs, чтобы сравнить производительность с моим кодом, и это бесшумно выходит из строя - код работает, но результирующий буфер содержит наименее значащий байт только ulong.
Тем не менее, я сравнивал производительность как большой буфер с, для initblk и моего метода memset. Время в мс составляет более 100 повторений, записывающих 8-байтовые улоны независимо от того, сколько раз соответствует длине буфера. Версия для версии вручную разворачивается по циклу для 8 байтов одного улунга.
Buffer Len #repeat For millisec Initblk millisec Memset millisec
0x00000008 100 For 0,0032 Initblk 0,0107 Memset 0,0052
0x00000010 100 For 0,0037 Initblk 0,0102 Memset 0,0039
0x00000020 100 For 0,0032 Initblk 0,0106 Memset 0,0050
0x00000040 100 For 0,0053 Initblk 0,0121 Memset 0,0106
0x00000080 100 For 0,0097 Initblk 0,0121 Memset 0,0091
0x00000100 100 For 0,0179 Initblk 0,0122 Memset 0,0102
0x00000200 100 For 0,0384 Initblk 0,0123 Memset 0,0126
0x00000400 100 For 0,0789 Initblk 0,0130 Memset 0,0189
0x00000800 100 For 0,1357 Initblk 0,0153 Memset 0,0170
0x00001000 100 For 0,2811 Initblk 0,0167 Memset 0,0221
0x00002000 100 For 0,5519 Initblk 0,0278 Memset 0,0274
0x00004000 100 For 1,1100 Initblk 0,0329 Memset 0,0383
0x00008000 100 For 2,2332 Initblk 0,0827 Memset 0,0864
0x00010000 100 For 4,4407 Initblk 0,1551 Memset 0,1602
0x00020000 100 For 9,1331 Initblk 0,2768 Memset 0,3044
0x00040000 100 For 18,2497 Initblk 0,5500 Memset 0,5901
0x00080000 100 For 35,8650 Initblk 1,1236 Memset 1,5762
0x00100000 100 For 71,6806 Initblk 2,2836 Memset 3,2323
0x00200000 100 For 77,8086 Initblk 2,1991 Memset 3,0144
0x00400000 100 For 131,2923 Initblk 4,7837 Memset 6,8505
0x00800000 100 For 263,2917 Initblk 16,1354 Memset 33,3719
Я исключил первый вызов каждый раз, поскольку как initblk, так и memset принимают удар, я полагаю, что это было около 0,22 мс для первого вызова. Немного удивительно, что мой код быстрее заполняет короткие буферы, чем initblk, так как он получил половину страницы, заполненную кодом настройки.
Если кто-то чувствует, как оптимизировать это, продолжайте действительно. Возможно.
Ответ 10
Вы можете сделать это, когда вы инициализируете массив, но я не думаю, что вы просите:
byte[] myBytes = new byte[5] { 1, 1, 1, 1, 1};
Ответ 11
Похоже, System.Runtime.CompilerServices.Unsafe.InitBlock
теперь делает то же самое, что и инструкция OpCodes.Initblk
о которой упоминает Конрад (он также упомянул ссылку на источник).
Код для заполнения массива выглядит следующим образом:
byte[] a = new byte[N];
byte valueToFill = 255;
System.Runtime.CompilerServices.Unsafe.InitBlock(ref a[0], valueToFill, (uint) a.Length);
Ответ 12
Протестировано несколько способов, описанных в разных ответах.
См. Источники тестов в С# тестовый класс
![контрольный отчет]()
Ответ 13
.NET Core имеет встроенную функцию Array.Fill(), но, к сожалению, в .NET Framework ее нет. .NET Core имеет два варианта: заполнить весь массив и заполнить часть массива, начиная с индекса.
Основываясь на вышеизложенных идеях, вот более общая функция Fill, которая заполнит весь массив данных нескольких типов. Это самая быстрая функция при сравнении с другими методами, обсуждаемыми в этом посте.
Эта функция вместе с версией, заполняющей часть массива, доступна в бесплатном пакете NuGet с открытым исходным кодом (HPCsharp на nuget.org). Также включена немного более быстрая версия Fill с использованием инструкций SIMD/SSE, которая выполняет только запись в память, тогда как методы на основе BlockCopy выполняют чтение и запись в память.
public static void FillUsingBlockCopy<T>(this T[] array, T value) where T : struct
{
int numBytesInItem = 0;
if (typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte))
numBytesInItem = 1;
else if (typeof(T) == typeof(ushort) || typeof(T) != typeof(short))
numBytesInItem = 2;
else if (typeof(T) == typeof(uint) || typeof(T) != typeof(int))
numBytesInItem = 4;
else if (typeof(T) == typeof(ulong) || typeof(T) != typeof(long))
numBytesInItem = 8;
else
throw new ArgumentException(string.Format("Type '{0}' is unsupported.", typeof(T).ToString()));
int block = 32, index = 0;
int endIndex = Math.Min(block, array.Length);
while (index < endIndex) // Fill the initial block
array[index++] = value;
endIndex = array.Length;
for (; index < endIndex; index += block, block *= 2)
{
int actualBlockSize = Math.Min(block, endIndex - index);
Buffer.BlockCopy(array, 0, array, index * numBytesInItem, actualBlockSize * numBytesInItem);
}
}
Ответ 14
У объекта Array есть метод Clear. Я готов поспорить, что метод Clear работает быстрее, чем любой код, который вы можете написать на С#.