2013-09-04 10 views
5

Sto lottando con l'implementazione di un buffer di memoria condiviso senza interrompere le rigide regole di aliasing di C99.

Supponiamo di avere un codice che elabora alcuni dati e che deve avere una memoria "scratch" per funzionare. Potrei scrivere come qualcosa di simile a:
Buffer di memoria condivisa in C++ senza violare le rigide regole di aliasing

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

Poi ho un altro funzione che fa un po 'di altre cose che ha anche bisogno di un buffer di zero:

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

Il problema è che foo() e bar () può essere chiamato più volte durante l'operazione e avere allocazioni di heap in tutto il luogo può essere piuttosto negativo in termini di prestazioni e frammentazione della memoria. Una soluzione ovvia sarebbe quella di allocare un buffer di memoria comune condivisa di dimensioni appropriate una volta e poi passare in foo() e bar() come argomento, BYOB stile:

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! 
    // ... 
} 


immagino I avere due domande ora:
- Come posso implementare un buffer di memoria scratch comune condiviso che non violi le regole di aliasing?
- Anche se il codice precedente viola le rigide regole di aliasing, non viene eseguito alcun "danno" con l'alias. Quindi un qualsiasi compilatore sano può generare codice (ottimizzato) che mi mette ancora nei guai?

Grazie

risposta

1

È sempre valido per interpretare un oggetto come una sequenza di byte (cioè è non una violazione aliasing trattare qualsiasi puntatore dell'oggetto come puntatore al primo elemento di un array di caratteri) e puoi costruire un oggetto in qualsiasi pezzo di memoria che sia abbastanza grande e adeguatamente allineato.

Quindi, è possibile allocare una vasta serie di char s (qualsiasi firma) e individuare un offset che è aliged a alignof(maxalign_t); ora puoi interpretare quel puntatore come un puntatore a oggetti una volta che hai costruito l'oggetto appropriato lì (ad esempio usando placement-new in C++).

Ovviamente è necessario assicurarsi di non scrivere nella memoria di un oggetto esistente; infatti, la durata dell'oggetto è intimamente legata a ciò che accade alla memoria che rappresenta l'oggetto.

Esempio:

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

Nota che la memoria ottiene malloc o new char[N] è sempre allineati per l'allineamento massima (ma non di più, e si potrebbe desiderare di utilizzare gli indirizzi over-allineati).

+0

"sempre valido per interpretare un oggetto come una sequenza di byte" ... solo per alcune operazioni. Ad esempio, l'operazione "Crea un oggetto di qualsiasi tipo all'interno" che si applica alle sequenze di byte non si applica a "un altro oggetto interpretato come una sequenza di byte". –

+0

@BenVoigt: Solo nella misura in cui è UB utilizzare l'archiviazione di un oggetto non banale per qualcos'altro prima di chiamare il distruttore ... Penso che non ci sia nulla di intrinsecamente sbagliato nell'istruzione. –

+2

Se si utilizza una memoria 'float' per creare un' int' all'interno, si apre la porta a violazioni rigorose di aliasing. In effetti, penso che il compilatore possa anche essere autorizzato a posticipare una scrittura al 'float' fino a dopo che la memoria viene riutilizzata per un' int' (corrompendo il valore di 'int'), perché l'aliasing rigoroso consente al compilatore di assumere che gli oggetti di diverso tipo non si sovrappongano. Poiché questa domanda riguarda in particolare le rigide regole di aliasing, ritengo che sia importante. La cosa migliore è usare la memoria iniziata come 'char []'. –

0

Se un'unione viene utilizzata per contenere le variabili int e float, è possibile passare l'aliasing rigoroso. Maggiori informazioni su questo argomento sono fornite in http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Vedere anche il seguente articolo.

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

egli dà un modo per utilizzare i sindacati per fare questo.

+1

Per questo scopo, probabilmente, ma 'union' non risolve i rigidi problemi di aliasing per la maggior parte delle persone che lo usano (leggendo un membro diverso da quello che è stato scritto). –

2

In realtà, ciò che hai scritto non è una violenta violazione dell'aliasing.

C++ 11 spec 3.10.10 dice:

Se un programma tenta di accedere al valore memorizzato di un oggetto attraverso un glvalue di diverso da uno dei seguenti tipi il comportamento è indefinito

Quindi la cosa che causa il comportamento indefinito è l'accesso al valore memorizzato, non solo la creazione di un puntatore ad esso. Il tuo esempio non viola nulla. Dovrebbe fare il passo successivo: float badValue = smem [0]. smem [0] ottiene il valore memorizzato dal buffer condiviso, creando una violazione di aliasing.

Naturalmente, non si sta per prendere smem [0] prima di impostarlo. Prima scriverai su di esso. Assegnare alla stessa memoria non accede al valore memorizzato, quindi niente ailiasing Tuttavia, è illegale scrivere sulla parte superiore di un oggetto mentre è ancora vivo. Per dimostrare che siamo al sicuro, abbiamo bisogno di durata di vita degli oggetti da 3.8.4:

un programma può porre fine alla vita di qualsiasi oggetto riutilizzando lo stoccaggio cui l'oggetto occupa o chiamando esplicitamente il distruttore per un oggetto di un tipo di classe con un distruttore non banale. Per un oggetto di un tipo di classe con un distruttore non banale, il programma non è obbligato a chiamare esplicitamente il distruttore prima che la memoria occupata dall'oggetto venga riutilizzata o rilasciata; ... [continua per quanto riguarda le conseguenze di distruttori non chiedono]

Hai un tipo di POD, distruttore così banale, così si può semplicemente dichiarare verbalmente "gli oggetti int sono tutti al termine della loro durata di vita, I' m usando lo spazio per i galleggianti. " Quindi riutilizza lo spazio per i float e non si verifica alcuna violazione di aliasing.

+0

Grazie, questo è molto utile. Se ti capisco bene, scrivere a 'scratchMem [i]' prima di leggere 'scratchMem [i]' sarà sempre al sicuro da un punto di vista dell'aliasing perché ho terminato la vita dell'oggetto nella posizione 'scratchMem + i' semplicemente scrivendo su di esso. È corretto? Inoltre, sono confuso riguardo al termine "riutilizzare la memoria". Come è definito? Ad esempio: 'int64_t a = 0; ((int16_t *) a) [1] = 1; '. La vita di 'a' è terminata con il secondo compito? Come appare un adeguato "riutilizzo dello spazio di archiviazione"? – rsp1984

+0

Come BenVoigt ha sottolineato in un altro commento, un compilatore può essere libero di parallelizzare/ritardare l'accesso alla memoria quando i puntatori sono di tipo diverso. Questo perché il compilatore presuppone che questi puntatori non siano alias. Quindi presumo che si possa ancora imbattersi in problemi di aliasing severo anche dopo aver impostato 'smem [0]' con un tipo float. Il compilatore potrebbe semplicemente essere libero di emettere istruzioni modificando la memoria su 'smem', interpretata come tipo int, dopodiché suppone che la memoria non si sovrapponga, cambiando così il valore di' smem [0] 'attraverso la porta di servizio. Gradirei comunque un chiarimento! – rsp1984

+0

I compilatori possono eseguire l'esecuzione solo fuori ordine se possono dimostrare che ciò non modifica i risultati di un programma validato. Poiché un programma può terminare la vita di un oggetto banalmente estraibile in qualsiasi momento riutilizzando lo spazio di archiviazione, il compilatore potrebbe non paralizzare queste attività a meno che non provi che la durata degli oggetti non è terminata. Se non fosse per questo, allora non c'è modo di scrivere "void * mem = malloc (max (sizeof (A), sizeof (B)); A * a = new (mem) A; a-> ~ A(); B * b = nuovo (mem) B; 'senza paura che i costruttori siano a corto di ordine –

Problemi correlati