2013-03-07 17 views
11

Ci scusiamo in anticipo per quello che potrebbe essere un primo post stupido su un terreno ben battuto. Mentre c'è molto materiale sull'argomento, molto poco è definitivo e/o comprensibile per me.C++: aliasing rigoroso contro abuso sindacale

Ho una classe modello AlignedArray per allocare dinamicamente memoria sullo heap con allineamento arbitrario (ho bisogno dell'allineamento di 32 byte per le routine di assemblaggio AVX). Ciò richiede una brutta manipolazione del puntatore.

Agner Fog fornisce una classe di esempio in cppexamples.zip che abusa di un unione per farlo (http://www.agner.org/optimize/optimization_manuals.zip). Tuttavia, so che scrivere a un membro di un sindacato e poi leggere da un altro risulta in UB.

AFAICT è sicuro fare l'alias di qualsiasi tipo di puntatore a char *, ma solo in una direzione. È qui che la mia comprensione diventa confusa. Ecco una versione ridotta del mio AlignedArray classe (essenzialmente una riscrittura di Agner di, per aiutare la mia comprensione):

template <typename T, size_t alignment = 32> 
class AlignedArray 
{ 
    size_t m_size; 
    char * m_unaligned; 
    T * m_aligned; 

public: 
    AlignedArray (size_t const size) 
     : m_size(0) 
     , m_unaligned(0) 
     , m_aligned(0) 
    { 
     this->size(size); 
    } 

    ~AlignedArray() 
    { 
     this->size(0); 
    } 

    T const & operator [] (size_t const i) const { return m_aligned[i]; } 

    T & operator [] (size_t const i) { return m_aligned[i]; } 

    size_t const size() { return m_size; } 

    void size (size_t const size) 
    { 
     if (size > 0) 
     { 
      if (size != m_size) 
      { 
       char * unaligned = 0; 
       unaligned = new char [size * sizeof(T) + alignment - 1]; 
       if (unaligned) 
       { 
        // Agner: 
        /* 
        union { 
         char * c; 
         T * t; 
         size_t s; 
        } aligned; 
        aligned.c = unaligned + alignment - 1; 
        aligned.s &= ~(alignment - 1); 
        */ 

        // Me: 
        T * aligned = reinterpret_cast<T *>((reinterpret_cast<size_t>(unaligned) + alignment - 1) & ~(alignment - 1)); 

        if (m_unaligned) 
        { 
         // Agner: 
         //memcpy(aligned.c, m_aligned, std::min(size, m_size)); 

         // Me: 
         memcpy(aligned, m_aligned, std::min(size, m_size)); 

         delete [] m_unaligned; 
        } 
        m_size = size; 
        m_unaligned = unaligned; 

        // Agner: 
        //m_aligned = aligned.t; 

        // Me: 
        m_aligned = aligned; 
       } 
       return; 
      } 
      return; 
     } 
     if (m_unaligned) 
     { 
      delete [] m_unaligned; 
      m_size = 0; 
      m_unaligned = 0; 
      m_aligned = 0; 
     } 
    } 
}; 

Quindi, quale metodo è sicuro (r)?

+3

Invece di costruire oggetti 'char' e poi colata che a T, perché non si afferra la memoria prima (da' operatore new' , o anche 'malloc'), come' void * ', e in realtà costruisci oggetti' T' in esso? Fondamentalmente: se vuoi oggetti T, costruisci oggetti T. Questo caso d'uso (allineamento allineato) ha * zero * bisogno di trucchi aliasing/sindacati/memcpy/qualunque. –

+0

@ R.MartinhoFernandes: Tranne che la matematica non è consentita su 'void *' s. Come si ottiene un 'vuoto *' allineato? – Omnifarious

+0

@Omnifarious Ultimo ho controllato, la matematica non è consentita su 'char *' neanche. (E anche se lo fosse, ciò non significherebbe che tu debba costruire oggetti char e non costruire oggetti T) Hai bisogno di interi per fare matematica. La soluzione portatile in C++ 11 è http://en.cppreference.com/w/cpp/memory/align. La soluzione teoricamente non-portatile è reinterpret_cast su un tipo numerico, eseguire i calcoli e reinterpretare il cast. (in pratica è abbastanza portatile perché in tutte le implementazioni so che reinterpret_cast a tipi numerici si comporta come previsto) –

risposta

3

Ho codice che implementa gli operatori (sostitutivi) new e , adatto per SIMD (ad esempio, SSE/AVX). Esso utilizza le seguenti funzioni che potreste trovare utili:

static inline void *G0__SIMD_malloc (size_t size) 
{ 
    constexpr size_t align = G0_SIMD_ALIGN; 
    void *ptr, *uptr; 

    static_assert(G0_SIMD_ALIGN >= sizeof(void *), 
        "insufficient alignment for pointer storage"); 

    static_assert((G0_SIMD_ALIGN & (G0_SIMD_ALIGN - 1)) == 0, 
        "G0_SIMD_ALIGN value must be a power of (2)"); 

    size += align; // raw pointer storage with alignment padding. 

    if ((uptr = malloc(size)) == nullptr) 
     return nullptr; 

    // size_t addr = reinterpret_cast<size_t>(uptr); 
    uintptr_t addr = reinterpret_cast<uintptr_t>(uptr); 

    ptr = reinterpret_cast<void *> 
     ((addr + align) & ~(align - 1)); 

    *(reinterpret_cast<void **>(ptr) - 1) = uptr; // (raw ptr) 

    return ptr; 
} 


static inline void G0__SIMD_free (void *ptr) 
{ 
    if (ptr != nullptr) 
     free(*(reinterpret_cast<void **>(ptr) - 1)); // (raw ptr) 
} 

questo dovrebbe essere facile da adattare. Ovviamente si sostituirà malloc e free, poiché si utilizza lo new globale e lo delete per l'archiviazione raw (char). Si presuppone che size_t sia sufficientemente ampio per l'aritmetica degli indirizzi - true nella pratica, ma uintptr_t da <cstdint> sarebbe più corretto.

+0

Su sistema POSIX, c'è posix_memalign() – BatchyX

+0

Grazie per questo, è molto utile (e stiamo entrambi facendo essenzialmente lo stesso puntatore munging). C'è qualche possibilità che uno dei nostri esempi possa portare a comportamenti indefiniti? Non sono sicuro se stiamo violando o meno le rigide regole di aliasing qui ... – linguamachina

+0

Se lo inserisci in un allocatore, puoi usare facilmente il resto della libreria standard: 'vector '; non c'è bisogno di reinventare qualcos'altro –

2

Per rispondere alla tua domanda, entrambi questi metodi sono altrettanto sicuri. Le uniche due operazioni che sono davvero maleodoranti sono il cast a size_t e new char[stuff]. Dovresti almeno usare uintptr_t da <cstdint> per il primo. La seconda operazione crea il tuo unico problema di alias del puntatore poiché tecnicamente il costruttore char viene eseguito su ciascun elemento char e che costituisce l'accesso ai dati tramite il puntatore char. Dovresti usare invece malloc.

L'altro presunto "puntatore aliasing" non è un problema. Questo perché oltre all'operazione new non si accede a nessun dato tramite i puntatori alias. Stai solo accedendo ai dati attraverso lo T * che ottieni dopo l'allineamento.

Ovviamente, è necessario ricordare di costruire tutti gli elementi dell'array. Questo è vero anche nella tua versione. Chissà che tipo di persone ci metteranno le T persone. E, naturalmente, se lo fai, dovrai ricordarti di chiamare i loro distruttori e ricordarti di gestire le eccezioni quando le copi (memcpy non lo taglia).

Se si dispone di una particolare funzionalità C++ 11, non è necessario eseguire questa operazione. C++ 11 ha una funzione specifica per allineare i puntatori a limiti arbitrari. L'interfaccia è un po 'funky, ma dovrebbe fare il lavoro. La chiamata è ::std::align definita in <memory>. Grazie a R. Martinho Fernandes per indicarlo.

Ecco una versione della funzione con il suggerito fisso:

#include <cstdint> // For uintptr_t 
#include <cstdlib> // For malloc 
#include <algorithm> 

template <typename T, size_t alignment = 32> 
class AlignedArray 
{ 
    size_t m_size; 
    void * m_unaligned; 
    T * m_aligned; 

public: 
    AlignedArray (size_t const size) 
     : m_size(0) 
     , m_unaligned(0) 
     , m_aligned(0) 
    { 
     this->size(size); 
    } 

    ~AlignedArray() 
    { 
     this->size(0); 
    } 

    T const & operator [] (size_t const i) const { return m_aligned[i]; } 

    T & operator [] (size_t const i) { return m_aligned[i]; } 

    size_t size() const { return m_size; } 

    void size (size_t const size) 
    { 
     using ::std::uintptr_t; 
     using ::std::malloc; 

     if (size > 0) 
     { 
      if (size != m_size) 
      { 
       void * unaligned = 0; 
       unaligned = malloc(size * sizeof(T) + alignment - 1); 
       if (unaligned) 
       { 
        T * aligned = reinterpret_cast<T *>((reinterpret_cast<uintptr_t>(unaligned) + alignment - 1) & ~(alignment - 1)); 

        if (m_unaligned) 
        { 
         ::std::size_t constructed = 0; 
         const ::std::size_t num_to_copy = ::std::min(size, m_size); 

         try { 
          for (constructed = 0; constructed < num_to_copy; ++constructed) { 
           new(aligned + constructed) T(m_aligned[constructed]); 
          } 
          for (; constructed < size; ++constructed) { 
           new(aligned + constructed) T; 
          } 
         } catch (...) { 
          for (::std::size_t i = 0; i < constructed; ++i) { 
           aligned[i].T::~T(); 
          } 
          ::std::free(unaligned); 
          throw; 
         } 

         for (size_t i = 0; i < m_size; ++i) { 
          m_aligned[i].T::~T(); 
         } 
         free(m_unaligned); 
        } 
        m_size = size; 
        m_unaligned = unaligned; 
        m_aligned = aligned; 
       } 
      } 
     } else if (m_unaligned) { // and size <= 0 
      for (::std::size_t i = 0; i < m_size; ++i) { 
       m_aligned[i].T::~T(); 
      } 
      ::std::free(m_unaligned); 
      m_size = 0; 
      m_unaligned = 0; 
      m_aligned = 0; 
     } 
    } 
}; 
+1

"L'alias del puntatore non è un problema, e questo perché non si accede a nessun dato tramite i puntatori alias". Riesco a vedere una serie di caratteri in costruzione, e quindi è accessibile tramite un T * ... –

+0

@ R.MartinhoFernandes: OK, hai ragione. E aggiusterò la mia risposta. – Omnifarious

+0

@ R.MartinhoFernandes: Lì, l'ho risolto. – Omnifarious