Лучший способ объединить два или более байтовых массивов в C #

239

У меня есть 3 байтовых массива в C #, которые мне нужно объединить в один. Какой будет наиболее эффективный метод для выполнения этой задачи?

Superdumbell
источник
3
Какие конкретно ваши требования? Вы берете объединение массивов или сохраняете несколько экземпляров одного и того же значения? Вы хотите отсортировать элементы или сохранить порядок в исходных массивах? Вы ищете эффективность в скорости или в строках кода?
Джейсон
Любите это, «лучшее» зависит от ваших требований.
Ади
7
Если вы можете использовать LINQ, то вы можете просто использовать Concatметод:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
casperOne
1
Пожалуйста, постарайтесь быть более четкими в своих вопросах. Этот смутный вопрос вызвал много путаницы среди этих людей, достаточно хороших, чтобы найти время, чтобы ответить вам.
Дрю Ноакс

Ответы:

327

Для примитивных типов (включая байты) используйте System.Buffer.BlockCopyвместо System.Array.Copy. Это быстрее

Я рассчитал каждый из предложенных методов в цикле, выполняемом 1 миллион раз, используя 3 массива по 10 байт каждый. Вот результаты:

  1. Использование нового массива байтов System.Array.Copy - 0,2187556 секунд
  2. Использование нового массива байтов System.Buffer.BlockCopy - 0,1406286 секунд
  3. IEnumerable <byte> с использованием оператора выхода C # - 0,0781270 секунд
  4. IEnumerable <byte> с использованием Concat LINQ <> - 0,0781270 секунд

Я увеличил размер каждого массива до 100 элементов и перезапустил тест:

  1. Новое использование System.Array.Copy байтового массива - 0,2812554 секунды
  2. Использование нового массива байтов System.Buffer.BlockCopy - 0,2500048 секунд
  3. IEnumerable <byte> с использованием оператора выхода C # - 0,0625012 секунды
  4. IEnumerable <byte> с использованием Concat LINQ <> - 0,0781265 секунд

Я увеличил размер каждого массива до 1000 элементов и перезапустил тест:

  1. Новое использование System.Array.Copy байтового массива - 1,0781457 секунд.
  2. Использование нового массива байтов System.Buffer.BlockCopy - 1,0156445 секунд
  3. IEnumerable <byte> с использованием оператора выхода C # - 0,0625012 секунды
  4. IEnumerable <byte> с использованием Concat LINQ <> - 0,0781265 секунд

Наконец, я увеличил размер каждого массива до 1 миллиона элементов и перезапустил тест, выполнив каждый цикл только 4000 раз:

  1. Использование System.Array.Copy нового байтового массива - 13,4533833 секунд
  2. Использование System.Buffer.BlockCopy нового байтового массива - 13.1096267 секунд
  3. IEnumerable <byte> с использованием оператора C # yield - 0 секунд
  4. IEnumerable <byte> с использованием Concat LINQ <> - 0 секунд

Итак, если вам нужен новый байтовый массив, используйте

byte[] rv = new byte[a1.Length + a2.Length + a3.Length];
System.Buffer.BlockCopy(a1, 0, rv, 0, a1.Length);
System.Buffer.BlockCopy(a2, 0, rv, a1.Length, a2.Length);
System.Buffer.BlockCopy(a3, 0, rv, a1.Length + a2.Length, a3.Length);

Но, если вы можете использовать IEnumerable<byte>, ОБЯЗАТЕЛЬНО предпочитают LINQ в Concat <> метод. Это только немного медленнее, чем оператор C # yield, но более лаконично и элегантно.

IEnumerable<byte> rv = a1.Concat(a2).Concat(a3);

Если у вас есть произвольное количество массивов и вы используете .NET 3.5, вы можете сделать System.Buffer.BlockCopyрешение более общим, например:

private byte[] Combine(params byte[][] arrays)
{
    byte[] rv = new byte[arrays.Sum(a => a.Length)];
    int offset = 0;
    foreach (byte[] array in arrays) {
        System.Buffer.BlockCopy(array, 0, rv, offset, array.Length);
        offset += array.Length;
    }
    return rv;
}

* Примечание: приведенный выше блок требует добавления следующего пространства имен в верхней части для его работы.

using System.Linq;

К точке зрения Джона Скита относительно итерации последующих структур данных (байтовый массив против IEnumerable <byte>) я перезапустил последний временной тест (1 миллион элементов, 4000 итераций), добавив цикл, который повторяется по всему массиву с каждым проходить:

  1. Использование нового массива байтов System.Array.Copy - 78,20550510 секунд
  2. Новое использование System.Buffer.BlockCopy байтового массива - 77,89261900 секунд
  3. IEnumerable <byte> с использованием оператора выхода C # - 551,7150161 секунд
  4. IEnumerable <byte> с использованием Concat LINQ <> - 448,1804799 секунд

Дело в том, что ОЧЕНЬ важно понимать эффективность как создания, так и использования полученной структуры данных. Простое сосредоточение внимания на эффективности создания может упускать из виду неэффективность, связанную с использованием. Слава, Джон.

Мэтт Дэвис
источник
61
Но действительно ли вы конвертируете его в массив в конце, как того требует вопрос? Если нет, конечно, это быстрее, но не соответствует требованиям.
Джон Скит
18
Re: Мэтт Дэвис - Неважно, нужно ли вашим «требованиям» превращать IEnumerable в массив - все, что нужно вашим требованиям, - это то, что результат действительно используется в некотором случае . Причина, по которой ваши тесты производительности на IEnumerable так низки, заключается в том, что вы на самом деле ничего не делаете ! LINQ не выполняет свою работу, пока вы не попытаетесь использовать результаты. По этой причине я нахожу ваш ответ объективно неправильным и может привести к тому, что другие будут использовать LINQ, когда им абсолютно не нужно, если они заботятся о производительности.
csauve
12
Я прочитал весь ответ, включая ваше обновление, мой комментарий стоит. Я знаю, что поздно вступаю в партию, но ответ вводит в заблуждение, и первая половина явно ложная .
csauve
14
Почему ответ, который содержит ложную и вводящую в заблуждение информацию, является голосом, получившим наибольшее количество голосов, и был отредактирован так, чтобы полностью аннулировать его первоначальное утверждение после того, как кто-то (Джон Скит) указал, что он даже не ответил на вопрос ОП?
MrCC
3
Вводящий в заблуждение ответ. Даже издание не отвечает на вопрос.
Серж Profafilecebook
154

Мне кажется, что многие ответы игнорируют заявленные требования:

  • Результатом должен быть байтовый массив
  • Это должно быть максимально эффективно

Вместе они исключают последовательность байтов LINQ - все, что с ними yield, сделает невозможным получение окончательного размера без итерации всей последовательности.

Если это, конечно, не реальные требования, LINQ может быть отличным решением (или IList<T>реализацией). Тем не менее, я предполагаю, что Superdumbell знает, что он хочет.

(РЕДАКТИРОВАТЬ: Я только что подумал. Существует большая семантическая разница между копированием массивов и их ленивым чтением. Подумайте, что произойдет, если вы измените данные в одном из «исходных» массивов после вызова Combine(или чего-то еще). ), но перед использованием результата - при ленивой оценке это изменение будет видно. При немедленном копировании этого не произойдет. В разных ситуациях потребуется другое поведение - просто что-то, о чем нужно знать.)

Вот мои предложенные методы - которые очень похожи на те, которые содержатся в некоторых других ответах, конечно :)

public static byte[] Combine(byte[] first, byte[] second)
{
    byte[] ret = new byte[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static byte[] Combine(byte[] first, byte[] second, byte[] third)
{
    byte[] ret = new byte[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static byte[] Combine(params byte[][] arrays)
{
    byte[] ret = new byte[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (byte[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}

Конечно, версия «params» требует сначала создания массива байтовых массивов, что вносит дополнительную неэффективность.

Джон Скит
источник
Джон, я точно понимаю, что ты говоришь. Единственное, что я хочу сказать, это то, что иногда вопросы задают с учетом конкретной реализации, не осознавая, что существуют другие решения. Просто дать ответ, не предлагая альтернатив, кажется плохой услугой для меня. Мысли?
Мэтт Дэвис
1
@Matt: Да, предлагать альтернативы - это хорошо, но стоит объяснить, что они являются альтернативами, а не выдавать их за ответ на вопрос. (Я не говорю, что вы это сделали - ваш ответ очень хороший.)
Джон Скит
4
(Хотя я думаю, что ваш тест производительности должен показывать время, необходимое для прохождения всех результатов в каждом случае, чтобы избежать ленивой оценки несправедливого преимущества.)
Джон Скит
1
Даже не удовлетворяя требованию «результат должен быть массивом», простое соблюдение требования «результат должен использоваться в некотором случае» сделает LINQ неоптимальным. Я думаю, что требование иметь возможность использовать результат должно быть неявным!
csauve
2
@andleer: Помимо всего прочего, Buffer.BlockCopy работает только с примитивными типами.
Джон Скит
44

Я взял пример LINQ Мэтта на шаг вперед для чистоты кода:

byte[] rv = a1.Concat(a2).Concat(a3).ToArray();

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

Нейт Барбеттини
источник
3
Краткое и простое решение, тест производительности был бы отличным!
Себастьян
3
Это, безусловно, понятно, доступно для чтения, не требует внешних библиотек / помощников, и, с точки зрения времени разработки, достаточно эффективно. Отлично, когда производительность во время выполнения не критична.
Бинки
28

Если вам просто нужен новый байтовый массив, используйте следующее:

byte[] Combine(byte[] a1, byte[] a2, byte[] a3)
{
    byte[] ret = new byte[a1.Length + a2.Length + a3.Length];
    Array.Copy(a1, 0, ret, 0, a1.Length);
    Array.Copy(a2, 0, ret, a1.Length, a2.Length);
    Array.Copy(a3, 0, ret, a1.Length + a2.Length, a3.Length);
    return ret;
}

В качестве альтернативы, если вам нужен только один IEnumerable, рассмотрите возможность использования оператора yield C # 2.0:

IEnumerable<byte> Combine(byte[] a1, byte[] a2, byte[] a3)
{
    foreach (byte b in a1)
        yield return b;
    foreach (byte b in a2)
        yield return b;
    foreach (byte b in a3)
        yield return b;
}
FryGuy
источник
Я сделал что-то похожее на ваш второй вариант, чтобы объединить большие потоки, работал как шарм. :)
Грег Д
2
Второй вариант отличный. +1.
Р. Мартиньо Фернандес
11

На самом деле я столкнулся с некоторыми проблемами при использовании Concat ... (с массивами в 10 миллионов он действительно потерпел крах).

Я обнаружил, что следующее простое, легкое и работает достаточно хорошо, без сбоев, и работает для ЛЮБОГО числа массивов (не только для трех) (используется LINQ):

public static byte[] ConcatByteArrays(params byte[][]  arrays)
{
    return arrays.SelectMany(x => x).ToArray();
}
00jt
источник
6

Класс памяти потока делает эту работу очень хорошо для меня. Я не мог заставить буферный класс работать так же быстро, как поток памяти.

using (MemoryStream ms = new MemoryStream())
{
  ms.Write(BitConverter.GetBytes(22),0,4);
  ms.Write(BitConverter.GetBytes(44),0,4);
  ms.ToArray();
}
Андрей
источник
3
Как и заявило qwe, я провел тест в цикле 10 000 000 раз, и MemoryStream вышел на 290% МЕНЬШЕ, чем Buffer.BlockCopy
esac
В некоторых случаях вы можете выполнять итерацию по множеству массивов без какого-либо предварительного знания длины отдельных массивов. Это хорошо работает в этом сценарии. BlockCopy полагается на предварительную обработку массива назначения
Sentinel
Как сказал @Sentinel, этот ответ идеально подходит для меня, потому что я не знаю размера вещей, которые я должен написать, и позволяет мне делать вещи очень чисто. Он также хорошо работает с [ReadOnly] Span <byte> в .NET Core 3!
Вода
Если вы инициализируете MemoryStream конечным размером, он не будет воссоздан и будет быстрее @esac.
Тоно Нам
2
    public static bool MyConcat<T>(ref T[] base_arr, ref T[] add_arr)
    {
        try
        {
            int base_size = base_arr.Length;
            int size_T = System.Runtime.InteropServices.Marshal.SizeOf(base_arr[0]);
            Array.Resize(ref base_arr, base_size + add_arr.Length);
            Buffer.BlockCopy(add_arr, 0, base_arr, base_size * size_T, add_arr.Length * size_T);
        }
        catch (IndexOutOfRangeException ioor)
        {
            MessageBox.Show(ioor.Message);
            return false;
        }
        return true;
    }
ЭДД
источник
К сожалению, это не будет работать со всеми типами. Marshal.SizeOf () не сможет вернуть размер для многих типов (попробуйте использовать этот метод с массивами строк, и вы увидите исключение "Тип 'System.String' не может быть маршалирован как неуправляемая структура; нет значимого размера или смещение может быть вычислено ". Вы можете попытаться ограничить параметр типа только ссылочными типами (добавив where T : struct), но - не будучи экспертом во внутренних средах CLR - я не могу сказать, можете ли вы получить исключения и для определенных структур. (например, если они содержат поля ссылочного типа)
Даниэль Скотт
2
    public static byte[] Concat(params byte[][] arrays) {
        using (var mem = new MemoryStream(arrays.Sum(a => a.Length))) {
            foreach (var array in arrays) {
                mem.Write(array, 0, array.Length);
            }
            return mem.ToArray();
        }
    }
Питер Эртл
источник
Ваш ответ мог бы быть лучше, если бы вы опубликовали небольшое объяснение того, что делает этот пример кода.
ПОПРАВИТЬ
1
он объединяет массив байтов в один большой байтовый массив (например): [1,2,3] + [4,5] + [6,7] ==> [1,2,3,4,5 , 6,7]
Питер Эртл
1

Можно использовать дженерики для объединения массивов. Следующий код может быть легко расширен до трех массивов. Таким образом, вам никогда не нужно дублировать код для различных типов массивов. Некоторые из приведенных выше ответов кажутся мне слишком сложными.

private static T[] CombineTwoArrays<T>(T[] a1, T[] a2)
    {
        T[] arrayCombined = new T[a1.Length + a2.Length];
        Array.Copy(a1, 0, arrayCombined, 0, a1.Length);
        Array.Copy(a2, 0, arrayCombined, a1.Length, a2.Length);
        return arrayCombined;
    }
BajaPaul
источник
0

Вот обобщение ответа, предоставленного @Jon Skeet. Это в основном то же самое, только для любого типа массива, а не только для байтов:

public static T[] Combine<T>(T[] first, T[] second)
{
    T[] ret = new T[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static T[] Combine<T>(T[] first, T[] second, T[] third)
{
    T[] ret = new T[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static T[] Combine<T>(params T[][] arrays)
{
    T[] ret = new T[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (T[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}
o_c
источник
3
ОПАСНОСТЬ! Эти методы не будут работать со свойствами любого типа массива с элементами длиной более одного байта (почти все, кроме байтовых массивов). Buffer.BlockCopy () работает с количеством байтов, а не с количеством элементов массива. Причина, по которой его можно легко использовать с байтовым массивом, состоит в том, что каждый элемент массива представляет собой один байт, поэтому физическая длина массива равна количеству элементов. Чтобы превратить методы byte [] Джона в универсальные методы, вам нужно умножить все смещения и длины на длину в байтах одного элемента массива - иначе вы не будете копировать все данные.
Даниэль Скотт
2
Обычно, чтобы это работало, вы вычисляете размер отдельного элемента sizeof(...)и умножаете его на количество элементов, которое хотите скопировать, но sizeof не может использоваться с универсальным типом. Можно - для некоторых типов - использовать Marshal.SizeOf(typeof(T)), но вы получите ошибки времени выполнения с определенными типами (например, строками). Кто-то с более глубоким знанием внутренней работы типов CLR сможет указать здесь все возможные ловушки. Достаточно сказать, что написание метода конкатенации универсального массива [с использованием BlockCopy] не тривиально.
Даниэль Скотт
2
И, наконец, вы можете написать общий метод конкатенации массива, подобный этому, почти так же, как показано выше (с немного меньшей производительностью), используя вместо этого Array.Copy. Просто замените все вызовы Buffer.BlockCopy на вызовы Array.Copy.
Даниэль Скотт
0
    /// <summary>
    /// Combine two Arrays with offset and count
    /// </summary>
    /// <param name="src1"></param>
    /// <param name="offset1"></param>
    /// <param name="count1"></param>
    /// <param name="src2"></param>
    /// <param name="offset2"></param>
    /// <param name="count2"></param>
    /// <returns></returns>
    public static T[] Combine<T>(this T[] src1, int offset1, int count1, T[] src2, int offset2, int count2) 
        => Enumerable.Range(0, count1 + count2).Select(a => (a < count1) ? src1[offset1 + a] : src2[offset2 + a - count1]).ToArray();
Мехмет ЮНЛЮ
источник
Спасибо за вклад. Поскольку на этот вопрос уже есть несколько высоко оцененных ответов более десяти лет назад, было бы полезно дать объяснение того, что отличает ваш подход. Почему кто-то должен использовать это вместо, например, принятого ответа?
Джереми Кейни
Мне нравится использовать расширенные методы, потому что есть понятный код для понимания. Этот код выбирает два массива с начальным индексом и счетчиком и конкатом. Также этот метод расширен. Итак, это для всех типов массивов, готовых на все времена
Mehmet ÜNLÜ
Это имеет смысл для меня! Вы не против изменить свой вопрос, чтобы включить эту информацию? Я думаю, что для будущих читателей было бы полезно иметь это заранее, чтобы они могли быстро отличить ваш подход от существующих ответов. Спасибо!
Джереми Кейни
-1

Все, что вам нужно, это передать список байтовых массивов, и эта функция вернет вам массив байтов (объединенный). Это лучшее решение, я думаю :).

public static byte[] CombineMultipleByteArrays(List<byte[]> lstByteArray)
        {
            using (var ms = new MemoryStream())
            {
                using (var doc = new iTextSharp.text.Document())
                {
                    using (var copy = new PdfSmartCopy(doc, ms))
                    {
                        doc.Open();
                        foreach (var p in lstByteArray)
                        {
                            using (var reader = new PdfReader(p))
                            {
                                copy.AddDocument(reader);
                            }
                        }

                        doc.Close();
                    }
                }
                return ms.ToArray();
            }
        }
ARC
источник
-5

Concat является правильным ответом, но по какой-то причине управляемая вещь получает большинство голосов. Если вам нравится этот ответ, возможно, вам нужно более общее решение:

    IEnumerable<byte> Combine(params byte[][] arrays)
    {
        foreach (byte[] a in arrays)
            foreach (byte b in a)
                yield return b;
    }

что позволит вам делать такие вещи, как:

    byte[] c = Combine(new byte[] { 0, 1, 2 }, new byte[] { 3, 4, 5 }).ToArray();
Марк Максем
источник
5
Вопрос, в частности, требует наиболее эффективного решения. Enumerable.ToArray не будет очень эффективным, так как он не может знать размер конечного массива, с которого можно начинать - в то время как методы, созданные вручную, могут.
Джон Скит