2013-09-04 12 views
5

Ich habe Schwierigkeiten, einen gemeinsamen Speicherpuffer zu implementieren, ohne die strengen Aliasregeln von C99 zu durchbrechen.

Angenommen, ich habe einen Code, der einige Daten verarbeitet und einige Arbeitsspeicher benötigt. Ich konnte es so etwas schreiben wie:
Shared Memory-Puffer in C++ ohne gegen strenge Aliasing-Regeln zu verstoßen

void foo(... some arguments here ...) { 
    int* scratchMem = new int[1000]; // Allocate. 
    // Do stuff... 
    delete[] scratchMem; // Free. 
} 

Dann habe ich eine andere Funktion, die einige andere Sachen tut, die auch einen Kratzer Puffer benötigt:

void bar(...arguments...) { 
    float* scratchMem = new float[1000]; // Allocate. 
    // Do other stuff... 
    delete[] scratchMem; // Free. 
} 

Das Problem ist, dass foo() und bar () kann während des Betriebs viele Male aufgerufen werden, und die Zuteilung von Heapspeicherplätzen kann in Bezug auf Leistung und Speicherfragmentierung ziemlich schlecht sein. Eine offensichtliche Lösung wäre, einen gemeinsamen, gemeinsam genutzten Speicherpuffer geeigneter Größe einmal zuzuteilen und dann gehen in foo() und bar() als Argument, BYOB-style:

void foo(void* scratchMem); 
void bar(void* scratchMem); 

int main() { 
    const int iAmBigEnough = 5000; 
    int* scratchMem = new int[iAmBigEnough]; 

    foo(scratchMem); 
    bar(scratchMem); 

    delete[] scratchMem; 
    return 0; 
} 

void foo(void* scratchMem) { 
    int* smem = (int*)scratchMem; 
    // Dereferencing smem will break strict-aliasing rules! 
    // ... 
} 

void bar(void* scratchMem) { 
    float* smem = (float*)scratchMem; 
    // Dereferencing smem will break strict-aliasing rules! 
    // ... 
} 


I ich denke, habe jetzt zwei Fragen:
- Wie kann ich einen gemeinsamen gemeinsamen Arbeitsspeicherpuffer implementieren, der nicht gegen Aliasregeln verstößt?
- Obwohl der obige Code gegen strenge Aliasing-Regeln verstößt, wird mit dem Alias ​​kein Schaden angerichtet. Könnte also jeder vernünftige Compiler (optimierten) Code erzeugen, der mich immer noch in Schwierigkeiten bringt?

Dank

Antwort

1

Es ist immer gültig, ein Objekt als eine Folge von Bytes zu interpretieren (dh es ist nicht eine Aliasing Verletzung jeden Objektzeiger als der Zeiger auf das erste Elemente eines Array von Zeichen zu behandeln) und Sie können ein Objekt in jedem Speicherstück konstruieren, das groß genug und passend ausgerichtet ist.

So können Sie ein großes Array von char s (jede signedness) zuordnen, und suchen Sie einen Offset, der mit alignof(maxalign_t); Jetzt können Sie diesen Zeiger als Objektzeiger interpretieren, nachdem Sie das entsprechende Objekt dort erstellt haben (z. B. mit placement-new in C++).

Sie müssen natürlich sicherstellen, dass Sie nicht in den Speicher eines vorhandenen Objekts schreiben; Tatsächlich ist die Objektlebenszeit eng mit dem verbunden, was mit dem Speicher geschieht, der das Objekt darstellt.

Beispiel:

char buf[50000]; 

int main() 
{ 
    uintptr_t n = reinterpret_cast<uintptr_t>(buf); 
    uintptr_t e = reinterpret_cast<uintptr_t>(buf + sizeof buf); 

    while (n % alignof(maxalign_t) != 0) { ++n; } 

    assert(e > n + sizeof(T)); 

    T * p = :: new (reinterpret_cast<void*>(n)) T(1, false, 'x'); 

    // ... 

    p->~T(); 
} 

Hinweis, dass der Speicher durch malloc oder new char[N] erhaltene immer für maximale Ausrichtung ausgerichtet (aber nicht mehr, und Sie können über ausrichte Adressen verwenden möchten).

+0

"immer gültig, um ein Objekt als eine Folge von Bytes zu interpretieren" ... nur für einige Operationen. Beispielsweise gilt die Operation "Objekt eines beliebigen Typs innerhalb erstellen", die für Bytefolgen gilt, nicht für "ein anderes Objekt, das als Bytefolge interpretiert wird". –

+0

@BenVoigt: Nur in dem Umfang, in dem UB den Speicher eines nicht-trivialen Objekts für etwas anderes verwendet, bevor er den Destruktor aufruft ... Ich denke, an der Anweisung liegt nichts an sich falsch. –

+2

Wenn Sie einen 'float's-Speicher verwenden, um einen' int 'innerhalb zu erstellen, öffnen Sie die Tür für strenge Aliasing-Verletzungen. Tatsächlich glaube ich, dass der Compiler sogar ein Schreiben in den "float" verzögern darf, bis der Speicher für ein "int" wiederverwendet wird (wodurch der Wert von "int" korrumpiert wird), da der Compiler vom strikten Aliasing ausgehen kann Objekte unterschiedlicher Art überlappen sich nicht. Da sich diese Frage speziell mit strengen Aliasing-Regeln beschäftigt, halte ich das für wichtig. Am besten ist es, Speicher zu verwenden, der als 'char []' gestartet wurde. –

0

Wenn eine Union die Variablen int und float enthält, können Sie das strikte Aliasing bestehen. Mehr dazu finden Sie in http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Siehe auch den folgenden Artikel.

http://blog.regehr.org/archives/959

Er gibt einen Weg, Gewerkschaften zu verwenden, dies zu tun.

+1

Für diesen Zweck, wahrscheinlich, aber "Union" löst nicht die strengen Aliasing-Probleme die meisten Leute verwenden es für (Lesen eines anderen Mitglieds als das, was geschrieben wurde). –

2

Eigentlich ist das, was Sie geschrieben haben, keine strenge Aliasing-Verletzung.

C++ 11 spec 3.10.10 sagt:

Wenn ein Programm versucht, den gespeicherten Wert eines Objekts durch eine glvalue anderer als einer der folgenden Typen zuzugreifen, das Verhalten nicht definiert ist

Die Sache, die das undefinierte Verhalten verursacht, ist der Zugriff auf den gespeicherten Wert, nicht nur einen Zeiger darauf zu erstellen. Ihr Beispiel verletzt nichts. Es müsste den nächsten Schritt machen: float badValue = slem [0]. slem [0] ruft den gespeicherten Wert aus dem gemeinsam genutzten Puffer ab und erzeugt eine Alias-Verletzung.

Natürlich sind Sie nicht gerade dabei, summ [0] zu nehmen, bevor Sie es einstellen. Du wirst zuerst darüber schreiben. Das Zuweisen zu demselben Speicher greift nicht auf den gespeicherten Wert zu, daher ist es nicht illegal, über das obere Ende eines Objekts zu schreiben, solange es noch lebt. Um zu beweisen, dass wir sicher sind, brauchen wir Objekt Lebensdauern von 3.8.4:

Ein Programm kann die Lebensdauer eines Objekts beenden, indem sie die Lagerung Wiederverwendung des das Objekt einnimmt oder durch Aufruf explizit den destructor für ein Objekt ein Klassentyp mit einem nicht-trivialen Destruktor. Für ein Objekt eines Klassentyps mit einem nicht-trivialen Destruktor muss das Programm den Destruktor nicht explizit aufrufen, bevor der Speicher, den das Objekt belegt, wiederverwendet oder freigegeben wird; ... [weiter Folgen nicht Aufruf Destruktoren auf in Bezug auf]

Sie einen POD-Typ haben, so trivial destructor, so können Sie einfach erklären verbal „die int-Objekte alle am Ende ihrer Lebensdauer sind, ich m den Platz für Schwimmer nutzen. " Sie verwenden dann den Speicherplatz für Gleitkommazahlen, und keine Alias-Verletzung tritt auf.

+0

Danke, das ist sehr hilfreich. Wenn ich Sie richtig verstehe, ist das Schreiben in 'scratchMem [i]' vor dem Lesen von 'scratchMem [i]' immer vor einem Aliasing-Standpunkt sicher, weil ich die Objektlebensdauer an der Stelle 'scratchMem + i' durch einfaches Schreiben beendet habe. Ist das korrekt? Außerdem bin ich verwirrt über den Begriff "Wiederverwendung des Speichers". Wie ist das definiert? Zum Beispiel: 'int64_t a = 0; ((int16_t *) a) [1] = 1; '. Ist 'a's Leben mit dem 2. Einsatz beendet? Wie sieht eine ordnungsgemäße "Wiederverwendung von Speicher" aus? – rsp1984

+0

Wie BenVoigt in einem anderen Kommentar darauf hingewiesen hat, kann ein Compiler den Zugriff auf den Speicher möglicherweise parallelisieren/verzögern, wenn Zeiger unterschiedlicher Art sind. Dies ist der Fall, da der Compiler davon ausgeht, dass diese Zeiger keinen Aliasnamen haben. Daher gehe ich davon aus, dass man auch nach dem Setzen von 'shm [0]' mit einem Float-Typ immer noch Probleme mit striktem Aliasing bekommen kann. Der Compiler kann nur frei sein, Befehle auszugeben, die den Speicher bei "smem" modifizieren, interpretiert als int-Typ, nachdem angenommen wird, dass sich der Speicher nicht überschneidet, wodurch der Wert von "smem [0]" durch die Hintertür geändert wird. Würde mich jedoch über eine Klarstellung freuen! – rsp1984

+0

Compiler dürfen nur außerhalb der Reihenfolge ausführen, wenn sie nachweisen können, dass dadurch die Ergebnisse eines gültig erstellten Programms nicht geändert werden. Da ein Programm die Lebensdauer eines trivial zerstörbaren Objekts jederzeit durch Wiederverwendung von Speicher beenden kann, kann der Compiler diese Aufgaben nicht parallelisieren, es sei denn, es kann beweisen, dass die Lebensdauern der Objekte nicht beendet sind. Wenn dies nicht der Fall wäre, dann kann man nicht schreiben: void mem = malloc (max (sizeof (A), sizeof (B)); A * a = neu (mem) A; a-> ~ A(); B * b = neu (mem) B; 'ohne Angst, dass die Konstrukteure außer Betrieb geraten würden –