Schnellste Möglichkeit, Massendaten zu überprüfen, wenn sie in C null sind? [duplicate]

Lesezeit: 9 Minuten

Benutzer-Avatar
Ringo_D

Ich habe eine Menge Daten, vielleicht 4 MB. Jetzt wollen wir prüfen, ob alle Bits darin 0 sind.

Beispiel: Hier sind die Daten:

void* data = malloc(4*1024*1024);
memset(data, 0, 4*1024*1024);

Überprüfen Sie, ob alle Bits darin 0 sind. Hier ist meine Lösung, die nicht schnell genug ist:

int dataisnull(char* data, int length)
{
    int i = 0;
    while(i<length){
        if (data[i]) return 0;
        i++;
    }
    return 1;
}

Dieser Code könnte einige Dinge haben, um die Leistung zu verbessern. Beispielsweise kann es bei einem 32/64-Bit-Computer schneller sein, 4/8 Bytes gleichzeitig zu prüfen.

Da frage ich mich, wie geht das am schnellsten?

  • Musst du nicht erhöhen data auch in der Schleife?

    – Andy Turner

    17. Februar 2016 um 7:20 Uhr


  • Verwenden Sie etwas wie SIMD, um die Daten schneller zu verarbeiten.

    – Dai

    17. Februar 2016 um 7:22 Uhr

  • Musst du auch nicht if(data[i]) return 0; ?

    – Magisch

    17. Februar 2016 um 7:24 Uhr

  • wenn Sie Daten zum Zeitpunkt der Zuordnung auf Null setzen möchten calloc ist viel schneller als malloc dann null

    – phuklv

    17. Februar 2016 um 7:53 Uhr

  • Mögliches Duplikat von Faster approach to check for a all-zero buffer in C?, obwohl diese Frage keine großartigen Antworten hat :/ Auch unter Verwendung von C/Intel-Assembly gefunden, was ist der schnellste Weg, um zu testen, ob ein 128-Byte-Speicher vorhanden ist Block enthält alle Nullen?

    – Peter Cordes

    17. Februar 2016 um 11:48 Uhr


Benutzer-Avatar
chqrlie

Sie können mehrere Bytes gleichzeitig verarbeiten und die Schleife entrollen:

int dataisnull(const void *data, size_t length) {
    /* assuming data was returned by malloc, thus is properly aligned */
    size_t i = 0, n = length / sizeof(size_t);
    const size_t *pw = data;
    const unsigned char *pb = data;
    size_t val;
#define UNROLL_FACTOR  8
#if UNROLL_FACTOR == 8
    size_t n1 = n - n % UNROLL_FACTOR;
    for (; i < n1; i += UNROLL_FACTOR) {
        val = pw[i + 0] | pw[i + 1] | pw[i + 2] | pw[i + 3] |
              pw[i + 4] | pw[i + 5] | pw[i + 6] | pw[i + 7];
        if (val)
            return 0;
    }
#endif
    val = 0;
    for (; i < n; i++) {
        val |= pw[i];
    }
    for (i = n * sizeof(size_t); i < length; i++) {
        val |= pb[i];
    }
    return val == 0;
}

Abhängig von Ihrem spezifischen Problem kann es effizienter sein, Werte ungleich Null früh oder spät zu erkennen:

  • Wenn der Nullfall am häufigsten vorkommt, sollten Sie alle Bits in berechnen val Akku und erst am Ende testen.
  • Wenn der Nullfall selten vorkommt, sollten Sie häufiger nach Nicht-Null-Werten suchen.

Die entrollte Version oben ist ein Kompromiss, der je nach Größe alle 64 oder 128 Bytes auf Nicht-Null-Werte testet size_t.

Abhängig von Ihrem Compiler und Prozessor erzielen Sie möglicherweise eine bessere Leistung, indem Sie weniger oder mehr entrollen. Sie könnten auch intrinsische Funktionen verwenden, die für Ihre spezielle Architektur verfügbar sind, um Vektortypen zu nutzen, aber dies wäre weniger portabel.

Beachten Sie, dass der Code die korrekte Ausrichtung für die nicht überprüft data Zeiger:

  • es kann nicht portabel gemacht werden.
  • Es wird davon ausgegangen, dass die Daten über zugewiesen wurden malloc oder ähnliches, daher für jeden Typ richtig ausgerichtet.

Vergleichen Sie wie immer verschiedene Lösungen, um zu sehen, ob es einen wirklichen Unterschied macht. Diese Funktion ist möglicherweise überhaupt kein Engpass, das Schreiben einer komplexen Funktion zur Optimierung eines seltenen Falls ist kontraproduktiv, sie macht den Code weniger lesbar, enthält mit größerer Wahrscheinlichkeit Fehler und ist viel weniger wartbar. Beispielsweise kann die Annahme zur Datenausrichtung möglicherweise nicht gelten, wenn Sie das Speicherzuweisungsschema ändern oder wenn Sie statische Arrays verwenden, die Funktion kann dann ein undefiniertes Verhalten hervorrufen.

  • Dieser Code ruft Undefiniertes Verhalten in C99, C11 oder C14 auf, wenn die fraglichen Daten einen effektiven Typ haben, der etwas anderes als ein size_t oder ein vorzeichenbehaftetes Äquivalent ist. Beachten Sie, dass für die Aliasing-Regeln von C99 int und long sind verschiedene Typen; auch wenn beide gleich groß sind size_t, höchstens einer von ihnen ist ein äquivalenter Typ für Aliasing-Regeln. Ihr Ansatz wäre in den meisten vernünftigen Dialekten von C gut, aber nicht in C99, C11 oder C14, wie von den Standards definiert.

    – Superkatze

    17. Februar 2016 um 8:22 Uhr

  • Compiler sind ziemlich komplex und unvorhersehbar. Ich mache mir Sorgen, dass der Compiler, da er die Ausrichtung des eingehenden Zeigers zur Kompilierzeit nicht überprüfen kann, teure Anweisungen erzeugt (viele schnelle Anweisungen funktionieren nur mit ausgerichteten Adressen). Außerdem kann ein Blick in den Assembler auf verschiedenen Optimierungsebenen zeigen, dass der Compiler bereits versucht, die Schleifen aufzurollen und die Zweige zu gruppieren (spekulative Ausführung).

    – Orion

    17. Februar 2016 um 8:31 Uhr

  • Bietet das Abrollen der Schleife tatsächlich eine Beschleunigung? Es sieht so aus, als ob der Compiler ziemlich gut darin sein sollte. GCC hat -funroll-loops um dies automatisch zu tun.

    – Davidmh

    17. Februar 2016 um 8:59 Uhr

  • @Ringo_D: Es sollte schneller sein, weil es weniger Tests und Verzweigungen gibt. weniger Vergleiche mit 0weniger gebundene Tests, da mehrere Bytes gleichzeitig gelesen und die Schleife entrollt werden.

    – chqrlie

    17. Februar 2016 um 10:40 Uhr

  • @supercat: Ich bestätige, dass der obige Kern auf einem DS9K möglicherweise nicht wie erwartet funktioniert: Ich konnte es dort nicht einmal testen, weil sich seine Zentraleinheit noch nicht von der letzten UB erholt hat, daemons immer noch ein- und ausfliegen.

    – chqrlie

    17. Februar 2016 um 10:48 Uhr

Benutzer-Avatar
Nicu Stiurca

Im Folgenden wird überprüft, ob das erste Byte Ihren Wünschen entspricht und alle nachfolgenden Bytepaare gleich sind.

int check_bytes(const char * const data, size_t length, const char val)
{
    if(length == 0) return 1;
    if(*data != val) return 0;
    return memcmp(data, data+1, length-1) ? 0 : 1;
}

int check_bytes64(const char * const data, size_t length, const char val)
{
    const char * const aligned64_start = (char *)((((uintptr_t)data) + 63) / 64 * 64);
    const char * const aligned64_end = (char *)((((uintptr_t)data) + length) / 64 * 64);
    const size_t start_length = aligned64_start - data;
    const size_t aligned64_length = aligned64_end - aligned64_start;
    const size_t end_length = length - start_length - aligned64_length;

    if (!check_bytes(data, start_length, val)) return 0;
    if (!check_bytes(aligned64_end, end_length, val)) return 0;

    return memcmp(aligned64_start, aligned64_start + 64, aligned64_length-64) ? 0 : 1;
}

Eine ausgefeiltere Version dieser Funktion sollte wahrscheinlich an Cache-Zeilen ausgerichtete Zeiger übergeben memcmpund überprüfen Sie die verbleibenden Blöcke manuell, anstatt nur das erste Byte.

Natürlich müssen Sie ein Profil auf Ihrer spezifischen Hardware erstellen, um festzustellen, ob diese Methode einen Geschwindigkeitsvorteil gegenüber anderen bietet.

Falls jemand zweifelt, ob das funktioniert, Idee.

  • Ich kenne auch den Grund für die Ablehnung nicht. Die Idee scheint wirklich schlau zu sein 🙂 und ich persönlich mag es. Daher ein Upvote von mir. Vielleicht ist eine unbedeutende Typumwandlung erforderlich, aber dies ist nur eine kosmetische Verbesserung.

    – dmi

    17. Februar 2016 um 7:30 Uhr


  • Diese Lösung ist nicht wirklich effizient: memcmp erhält nicht ausgerichtete Zeiger: Es kann kein optimierter Code verwendet werden, der mehrere Bytes gleichzeitig effizient vergleicht. Außerdem ist das Vergleichen von Bytes aus dem Speicher weniger effizient als das Vergleichen von Bytes mit einem konstanten Wert. Ich habe nicht abgelehnt, aber ich verstehe diejenigen, die es getan haben.

    – chqrlie

    17. Februar 2016 um 7:53 Uhr

  • @chqrlie nicht unbedingt, es hängt von der Implementierung von memcmp ab. Einige von ihnen überprüfen, ob die Zeiger ausgerichtet sind, und ändern den Algorithmus entsprechend.

    – Jabberwocky

    17. Februar 2016 um 7:59 Uhr

  • @MichaelWalz Ich denke, sein Punkt war das wegen data, data+1beide Zeiger kann nicht ausgerichtet werden.

    – Benutzer694733

    17. Februar 2016 um 8:06 Uhr


  • @supercat Strenge Aliasing-Regeln (oder andere Sprachregeln) gelten nicht für die Implementierung von C-Standardbibliotheksfunktionen. So lange wie memcmp verhält sich wie der Standard sagt, der Code kann alle gewünschten Typen verwenden. Funktionen müssen nicht einmal in C geschrieben werden.

    – Benutzer694733

    17. Februar 2016 um 9:14 Uhr

Benutzer-Avatar
5gon12eder

Ich habe einmal die folgende Funktion für meinen eigenen Gebrauch geschrieben. Es wird davon ausgegangen, dass die zu prüfenden Daten ein Vielfaches einer konstanten Blockgröße sind und für einen Puffer von Maschinenwörtern richtig ausgerichtet sind. Wenn dies in Ihrem Fall nicht gegeben ist, ist es nicht schwierig, die ersten und letzten paar Bytes einzeln zu loopen und nur die Masse mit der optimierten Funktion zu überprüfen. (Genau genommen ist es ein undefiniertes Verhalten, selbst wenn das Array richtig ausgerichtet ist, die Daten jedoch von einem Typ geschrieben wurden, der nicht kompatibel ist unsigned long. Ich glaube jedoch, dass Sie mit diesem vorsichtigen Regelbruch hier ziemlich weit kommen können.)

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

bool
is_all_zero_bulk(const void *const p, const size_t n)
{
  typedef unsigned long word_type;
  const size_t word_size = sizeof(word_type);
  const size_t chunksize = 8;
  assert(n % (chunksize * word_size) == 0);
  assert((((uintptr_t) p) & 0x0f) == 0);
  const word_type *const frst = (word_type *) p;
  const word_type *const last = frst + n / word_size;
  for (const word_type * iter = frst; iter != last; iter += chunksize)
    {
      word_type acc = 0;
      // Trust the compiler to unroll this loop at its own discretion.
      for (size_t j = 0; j < chunksize; ++j)
        acc |= iter[j];
      if (acc != 0)
        return false;
    }
  return true;
}

Die Funktion selbst ist nicht sehr schlau. Die wichtigsten Ideen sind:

  • Verwenden Sie große vorzeichenlose Maschinenwörter für den Datenvergleich.
  • Aktivieren Sie das Abrollen von Schleifen, indem Sie eine innere Schleife mit einer konstanten Iterationsanzahl ausklammern.
  • Reduzieren Sie die Anzahl der Verzweigungen, indem Sie die Wörter in einen Akkumulator ODER-verknüpfen und ihn nur alle paar Iterationen mit Null vergleichen.
  • Dies sollte es dem Compiler auch leicht machen, vektorisierten Code mit zu generieren SIMD Anweisungen, die Sie wirklich für Code wie diesen wollen.

Zusätzliche nicht standardmäßige Optimierungen wären, die Funktion mit zu kommentieren __attribute__ ((hot)) und verwenden __builtin_expect(acc != 0, false). Das Wichtigste ist natürlich, die Optimierungen Ihres Compilers einzuschalten.

  • Beachten Sie, dass die Funktion in C99, C11 und C14 Undefiniertes Verhalten aufruft, wenn die Daten als ein anderer Typ als word_type oder ein vorzeichenbehaftetes Äquivalent davon geschrieben wurden. Vielleicht wird das Standards Committee eines Tages anerkennen, dass eine anständige Sprache ein Mittel zur Verwendung von Typen, die größer als “char” sind, enthalten sollte, um “Rohspeicher” zu manipulieren, aber das ist noch nicht der Fall.

    – Superkatze

    17. Februar 2016 um 8:28 Uhr

  • @supercat Ja, ich weiß. Es ist ein „Daumen drücken“-Ansatz.

    – 5gon12eder

    17. Februar 2016 um 8:30 Uhr

1334760cookie-checkSchnellste Möglichkeit, Massendaten zu überprüfen, wenn sie in C null sind? [duplicate]

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy