Najlepszy sposób na połączenie dwóch lub więcej tablic bajtowych w C #

238

Mam 3 bajty tablic w języku C #, które muszę połączyć w jeden. Jaka byłaby najbardziej wydajna metoda wykonania tego zadania?

Superdumbell
źródło
3
Jakie konkretnie są twoje wymagania? Czy bierzesz połączenie tablic, czy zachowujesz wiele wystąpień o tej samej wartości? Czy chcesz posortować elementy, czy też zachować porządek w początkowych tablicach? Szukasz wydajności w szybkości lub w liniach kodu?
jason
Uwielbiam to, „najlepsze” zależy od twoich wymagań.
Ady
7
Jeśli możesz korzystać z LINQ, możesz po prostu użyć Concatmetody:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
casperOne
1
Postaraj się być bardziej przejrzysty w swoich pytaniach. To niejasne pytanie spowodowało wiele zamieszania wśród osób wystarczająco dobrych, aby poświęcić czas na udzielenie odpowiedzi.
Drew Noakes,

Odpowiedzi:

326

W przypadku typów pierwotnych (w tym bajtów) użyj System.Buffer.BlockCopyzamiast System.Array.Copy. To jest szybsze.

Ustawiłem każdą z sugerowanych metod w pętli wykonanej 1 milion razy, stosując 3 tablice po 10 bajtów każda. Oto wyniki:

  1. Nowa tablica bajtów przy użyciu System.Array.Copy - 0,2187556 sekund
  2. Nowa tablica bajtów przy użyciu System.Buffer.BlockCopy - 0,1406286 sekund
  3. IEnumerable <byte> przy użyciu operatora C # - 0,0781270 sekund
  4. IEnumerable <byte> przy użyciu Concat LINQ <> - 0,0781270 sekund

Zwiększyłem rozmiar każdej tablicy do 100 elementów i ponownie uruchomiłem test:

  1. Nowa tablica bajtów przy użyciu System.Array.Copy - 0,2812554 sekund
  2. Nowa tablica bajtów przy użyciu System.Buffer.BlockCopy - 0,2500048 sekund
  3. IEnumerable <byte> przy użyciu operatora C # - 0,0625012 sekund
  4. IEnumerable <byte> przy użyciu Concat LINQ <> - 0,0781265 sekund

Zwiększyłem rozmiar każdej tablicy do 1000 elementów i ponownie uruchomiłem test:

  1. Nowa tablica bajtów przy użyciu System.Array.Copy - 1.0781457 sekund
  2. Nowa tablica bajtów przy użyciu System.Buffer.BlockCopy - 1.0156445 sekund
  3. IEnumerable <byte> przy użyciu operatora C # - 0,0625012 sekund
  4. IEnumerable <byte> przy użyciu Concat LINQ <> - 0,0781265 sekund

Na koniec zwiększyłem rozmiar każdej tablicy do 1 miliona elementów i ponownie uruchomiłem test, wykonując każdą pętlę tylko 4000 razy:

  1. Nowa tablica bajtów przy użyciu System.Array.Copy - 13,4533833 sekund
  2. Nowa tablica bajtów przy użyciu System.Buffer.BlockCopy - 13.1096267 sekund
  3. IEnumerable <byte> przy użyciu operatora C # - 0 sekund
  4. IEnumerable <byte> przy użyciu Concat LINQ <> - 0 sekund

Więc jeśli potrzebujesz nowej tablicy bajtów, użyj

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);

Ale jeśli możesz użyć IEnumerable<byte>, ZDECYDOWANIE wolisz metodę Concat <> LINQ. Jest tylko nieco wolniejszy niż operator plonu C #, ale jest bardziej zwięzły i bardziej elegancki.

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

Jeśli masz dowolną liczbę tablic i korzystasz z .NET 3.5, możesz uczynić to System.Buffer.BlockCopyrozwiązanie bardziej ogólnym:

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;
}

* Uwaga: Powyższy blok wymaga dodania następującej przestrzeni nazw u góry, aby działał.

using System.Linq;

Do punktu Jona Skeeta dotyczącego iteracji kolejnych struktur danych (tablica bajtów vs. IEnumerable <byte>) ponownie uruchomiłem ostatni test synchronizacji (1 milion elementów, 4000 iteracji), dodając pętlę, która iteruje pełną tablicę z każdym przechodzić:

  1. Nowa tablica bajtów przy użyciu System.Array.Copy - 78,20550510 sekund
  2. Nowa tablica bajtów przy użyciu System.Buffer.BlockCopy - 77,89261900 sekund
  3. IEnumerable <byte> przy użyciu operatora C # - 551,7150161 sekund
  4. IEnumerable <byte> przy użyciu Concat LINQ <> - 448.1804799 sekund

Chodzi o to, że BARDZO ważne jest zrozumienie wydajności zarówno tworzenia, jak i wykorzystania wynikowej struktury danych. Skupienie się na wydajności kreacji może przeoczyć nieefektywność związaną z użyciem. Kudos, Jon.

Matt Davis
źródło
61
Ale czy na końcu konwertujesz go do tablicy, jak wymaga tego pytanie? Jeśli nie, to oczywiście szybciej - ale nie spełnia wymagań.
Jon Skeet
18
Re: Matt Davis - Nie ma znaczenia, czy twoje „wymagania” wymagają przekształcenia IEnumerable w tablicę - wszystko czego potrzebujesz, to fakt, że wynik jest faktycznie używany w jakiejś fasadzie . Powodem, dla którego twoje testy wydajności na IEnumerable są tak niskie, jest to, że tak naprawdę nic nie robisz ! LINQ nie wykonuje żadnej ze swoich prac, dopóki nie spróbujesz użyć wyników. Z tego powodu uważam twoją odpowiedź za obiektywnie niepoprawną i może skłonić innych do korzystania z LINQ, gdy absolutnie nie powinni, jeśli zależy im na wydajności.
csauve
12
Przeczytałem całą odpowiedź, w tym twoją aktualizację, mój komentarz jest ważny. Wiem, że dołączam do drużyny późno, ale odpowiedź jest rażąco myląca, a pierwsza połowa jest oczywiście fałszywa .
csauve
14
Dlaczego odpowiedź, która zawiera fałszywe i wprowadzające w błąd informacje, jest najczęściej głosowaną odpowiedzią i została zredagowana w celu całkowitego unieważnienia jej oryginalnego oświadczenia po tym, jak ktoś (Jon Skeet) zauważył, że nawet nie odpowiedział na pytanie OP?
MrCC
3
Myląca odpowiedź. Nawet wydanie nie odpowiada na pytanie.
Serge Profafilecebook
154

Wydaje mi się, że wiele odpowiedzi ignoruje podane wymagania:

  • Wynik powinien być tablicą bajtów
  • Powinien być tak wydajny, jak to możliwe

Te dwa razem wykluczają sekwencję bajtów LINQ - wszystko, yieldco może uniemożliwić uzyskanie ostatecznego rozmiaru bez iteracji przez całą sekwencję.

Jeśli to nie są prawdziwe wymagania, LINQ może być idealnie dobrym rozwiązaniem (lub IList<T>implementacją). Zakładam jednak, że Superdumbell wie, czego chce.

(EDYCJA: Właśnie pomyślałem. Jest duża różnica semantyczna między tworzeniem kopii tablic a leniwym ich czytaniem. Zastanów się, co się stanie, jeśli zmienisz dane w jednej z tablic „źródłowych” po wywołaniu Combine(lub cokolwiek innego) ), ale przed użyciem wyniku - przy leniwej ocenie, zmiana będzie widoczna. W przypadku natychmiastowej kopii nie będzie. Różne sytuacje będą wymagały innego zachowania - po prostu coś, o czym należy pamiętać.)

Oto moje proponowane metody - które są bardzo podobne do tych zawartych w niektórych innych odpowiedziach, na pewno :)

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;
}

Oczywiście wersja „params” wymaga najpierw utworzenia tablicy tablic bajtów, co wprowadza dodatkową nieefektywność.

Jon Skeet
źródło
Jon, rozumiem dokładnie, co mówisz. Chodzi mi tylko o to, że czasami zadawane są pytania z myślą o konkretnej implementacji, nie zdając sobie sprawy z istnienia innych rozwiązań. Samo udzielenie odpowiedzi bez oferowania alternatyw wydaje mi się niekorzystne. Myśli?
Matt Davis
1
@Matt: Tak, oferowanie alternatyw jest dobre - ale warto wyjaśnić, że one alternatywami, zamiast podawać je jako odpowiedź na zadawane pytanie. (Nie twierdzę, że to zrobiłeś - twoja odpowiedź jest bardzo dobra.)
Jon Skeet
4
(Chociaż myślę, że twój test wydajności powinien również pokazywać czas potrzebny na przejrzenie wszystkich wyników w każdym przypadku, aby uniknąć leniwej oceny nieuczciwej przewagi.)
Jon Skeet
1
Nawet bez spełnienia wymogu „wynik musi być tablicą”, samo spełnienie wymogu „wynik musi być użyty w jakimś fasionie” sprawiłoby, że LINQ nie byłby optymalny. Myślę, że wymóg, aby móc skorzystać z wyniku, powinien być dorozumiany!
csauve
2
@andleer: Oprócz czegokolwiek innego, Buffer.BlockCopy działa tylko z typami pierwotnymi.
Jon Skeet
44

Wziąłem przykład Matta o LINQ o krok dalej w kwestii czystości kodu:

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

W moim przypadku tablice są małe, więc nie martwię się o wydajność.

Nate Barbettini
źródło
3
Krótkie i proste rozwiązanie, test wydajności byłby świetny!
Sebastian
3
Jest to zdecydowanie jasne, czytelne, nie wymaga zewnętrznych bibliotek / pomocników, a pod względem czasu programowania jest dość wydajne. Świetne, gdy wydajność w czasie wykonywania nie jest krytyczna.
binki
28

Jeśli potrzebujesz nowej tablicy bajtów, skorzystaj z następujących opcji:

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;
}

Alternatywnie, jeśli potrzebujesz tylko jednego IEnumerable, rozważ użycie operatora wydajności 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
źródło
Zrobiłem coś podobnego do drugiej opcji łączenia dużych strumieni, działając jak urok. :)
Greg D
2
Druga opcja jest świetna. +1.
R. Martinho Fernandes
11

Właściwie napotkałem pewne problemy z używaniem Concat ... (z tablicami na 10 milionach, faktycznie się zawiesił).

Odkryłem, że poniższe są proste, łatwe i działają wystarczająco dobrze, nie powodując awarii, i działa na KAŻDEJ liczbie tablic (nie tylko trzech) (używa LINQ):

public static byte[] ConcatByteArrays(params byte[][]  arrays)
{
    return arrays.SelectMany(x => x).ToArray();
}
00jt
źródło
6

Klasa memorystream wykonuje tę pracę dla mnie całkiem nieźle. Nie mogłem uruchomić klasy bufora tak szybko, jak memorystream.

using (MemoryStream ms = new MemoryStream())
{
  ms.Write(BitConverter.GetBytes(22),0,4);
  ms.Write(BitConverter.GetBytes(44),0,4);
  ms.ToArray();
}
Andrzej
źródło
3
Jak powiedziałem qwe, zrobiłem test w pętli 10 000 000 razy, a MemoryStream wyszedł 290% wolniej niż bufor. BlockCopy
esac
W niektórych przypadkach możesz iterować po wyliczanej liczbie tablic bez uprzedniej wiedzy o poszczególnych długościach tablic. Działa to dobrze w tym scenariuszu. BlockCopy polega na przygotowaniu tablicy docelowej
Sentinel
Jak powiedział @Sentinel, ta odpowiedź jest dla mnie idealna, ponieważ nie mam wiedzy na temat wielkości rzeczy, które muszę pisać i pozwala mi robić wszystko bardzo czysto. Dobrze gra również w .NET Core 3 w [ReadOnly] Span <byte>!
Woda,
Jeśli zainicjujesz MemoryStream końcowym rozmiarem rozmiaru, nie zostanie on odtworzony i będzie szybszy @esac.
Tono Nam
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;
    }
edd
źródło
Niestety nie będzie to działać ze wszystkimi typami. Marshal.SizeOf () nie będzie w stanie zwrócić rozmiaru dla wielu typów (spróbuj użyć tej metody z tablicami łańcuchów, a zobaczysz wyjątek „Typ 'System.String” nie może być zestawiony jako struktura niezarządzana; brak znaczącego rozmiaru lub offset można obliczyć ". Możesz spróbować ograniczyć parametr typu tylko do typów referencyjnych (dodając where T : struct), ale - nie będąc ekspertem w wewnętrznych CLR - nie mogłem powiedzieć, czy możesz uzyskać wyjątki również dla niektórych struktur (np. jeśli zawierają pola typu odniesienia)
Daniel Scott
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();
        }
    }
Peter Ertl
źródło
Twoja odpowiedź mogłaby być lepsza, gdybyś zamieścił małe wyjaśnienie tego, co zawiera ten przykładowy kod.
AFract
1
konkatenuje tablicę bajtów w jedną dużą tablicę bajtów (tak jak to): [1,2,3] + [4,5] + [6,7] ==> [1,2,3,4,5 , 6,7]
Peter Ertl,
1

Może używać rodzajów ogólnych do łączenia tablic. Poniższy kod można łatwo rozszerzyć do trzech tablic. W ten sposób nigdy nie trzeba duplikować kodu dla różnych typów tablic. Niektóre z powyższych odpowiedzi wydają mi się zbyt skomplikowane.

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
źródło
0

Oto uogólnienie odpowiedzi udzielonej przez @Jona Skeeta. Jest w zasadzie taki sam, tylko można go używać do dowolnego typu tablicy, nie tylko bajtów:

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
źródło
3
ZAGROŻENIE! Te metody nie będą działać z właściwością dowolnego typu tablicy z elementami dłuższymi niż jeden bajt (prawie wszystko inne niż tablice bajtów). Buffer.BlockCopy () działa z ilością bajtów, a nie z liczbą elementów tablicy. Powodem, dla którego można go łatwo używać z tablicą bajtów, jest to, że każdy element tablicy jest jednym bajtem, więc fizyczna długość tablicy jest równa liczbie elementów. Aby zamienić metody bajtowe [] Johna na metody ogólne, musisz pomnożyć wszystkie przesunięcia i długości przez długość bajtu pojedynczego elementu tablicy - w przeciwnym razie nie skopiujesz wszystkich danych.
Daniel Scott
2
Zwykle, aby to zadziałało, należy obliczyć rozmiar pojedynczego elementu za pomocą sizeof(...)i pomnożyć go przez liczbę elementów, które chcesz skopiować, ale sizeof nie można używać z typem ogólnym. Możliwe jest - w przypadku niektórych typów - użycie Marshal.SizeOf(typeof(T)), ale w przypadku niektórych typów (np. Łańcuchów) pojawią się błędy w czasie wykonywania. Ktoś z bardziej dogłębną znajomością wewnętrznego działania typów CLR będzie w stanie wskazać tutaj wszystkie możliwe pułapki. Wystarczy powiedzieć, że napisanie ogólnej metody konkatenacji tablic [przy użyciu BlockCopy] nie jest trywialne.
Daniel Scott
2
I na koniec - możesz napisać taką ogólną metodę konkatenacji tablic w prawie dokładnie taki sposób, jak pokazano powyżej (z nieco niższą wydajnością), używając zamiast tego Array.Copy. Wystarczy zastąpić wszystkie wywołania Buffer.BlockCopy wywołaniami Array.Copy.
Daniel Scott
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Ü
źródło
Dziękuję za wkład. Ponieważ istnieje już wiele wysoko ocenianych odpowiedzi na to pytanie ponad dziesięć lat temu, przydatne byłoby wyjaśnienie tego, co wyróżnia twoje podejście. Dlaczego ktoś miałby to wykorzystać zamiast np. Zaakceptowanej odpowiedzi?
Jeremy Caney
Lubię korzystać z rozszerzonych metod, ponieważ są jasne kody do zrozumienia. Kod ten wybiera dwie tablice z indeksem początkowym oraz zliczaniem i konkatacją. Również ta metoda została rozszerzona.
Dotyczy
To ma dla mnie sens! Czy masz coś przeciwko edycji pytania, aby uwzględnić te informacje? Myślę, że przydałoby się to z wyprzedzeniem czytelnikom, aby mogli szybko odróżnić twoje podejście od istniejących odpowiedzi. Dziękuję Ci!
Jeremy Caney
-1

Wystarczy przekazać listę tablic bajtów, a ta funkcja zwróci tablicę bajtów (scalone). Myślę, że to najlepsze rozwiązanie :).

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();
            }
        }
ŁUK
źródło
-5

Concat jest właściwą odpowiedzią, ale z jakiegoś powodu najwięcej głosów zdobywa się na liście kontrolnej. Jeśli podoba ci się ta odpowiedź, być może chciałbyś jeszcze bardziej ogólne rozwiązanie:

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

co pozwoli ci robić takie rzeczy jak:

    byte[] c = Combine(new byte[] { 0, 1, 2 }, new byte[] { 3, 4, 5 }).ToArray();
Mark Maxham
źródło
5
Pytanie dotyczy w szczególności najbardziej wydajnego rozwiązania. Enumerable.ToArray nie będzie bardzo wydajny, ponieważ nie może znać rozmiaru ostatecznej tablicy na początek - podczas gdy techniki zwijane ręcznie mogą.
Jon Skeet