Reutilización de un búfer flotante para dobles sin comportamiento indefinido


En una función particular de C++, resulta que tengo un puntero a un gran búfer de flotadores que quiero usar temporalmente para almacenar la mitad del número de dobles. ¿Existe un método para usar este búfer como espacio de scratch para almacenar los dobles, que también está permitido (es decir, no es un comportamiento indefinido) por el estándar?

En resumen, me gustaría esto:

void f(float* buffer)
{
  double* d = reinterpret_cast<double*>(buffer);
  // make use of d
  d[i] = 1.;
  // done using d as scratch, start filling the buffer
  buffer[j] = 1.;
}

Por lo que veo no hay una manera fácil de hacer esto: si entiendo correctamente, un reinterpret_cast<double*> como este causa un comportamiento indefinido debido al aliasing de tipos, y el uso de memcpy o un float/double union no es posible sin copiar los datos y asignar espacio adicional, lo que frustra el propósito y resulta costoso en mi caso (y el uso de una unión para el juego de tipos no está permitido en C++).

Se puede asumir que el búfer flotante está correctamente alineado para usarlo para dobles.

Author: Boann, 2018-07-11

6 answers

Creo que el siguiente código es una forma válida de hacerlo (en realidad es solo un pequeño ejemplo sobre la idea):

#include <memory>

void f(float* buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];

    // we have started the lifetime of the doubles.
    // "d" is a new pointer pointing to the first double object in the array.        
    // now you can use "d" as a double buffer for your calculations
    // you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"       
    d[0] = 1.;
    // do some work here on/with the doubles...


    // conceptually we need to destory the doubles here... but they are trivially destructable

    // now we need to start the lifetime of the floats again
    new (buffer) float[10];  


    // here we are unsure about wether we need to update the "buffer" pointer to 
    // the one returned by the placement new of the floats
    // if it is nessessary, we could return the new float pointer or take the input pointer
    // by reference and update it directly in the function
}

int main()
{
    float* floats = new float[10];
    f(floats, sizeof(float) * 10);
    return 0;
}

Es importante que solo utilice el puntero que recibe de la colocación nuevo. Y es importante para la colocación de nuevo detrás de los flotadores. Incluso si se trata de una construcción sin operación, debe comenzar de nuevo la vida útil de los flotadores.

Olvídate de std::launder y reinterpret_cast en los comentarios. La colocación nueva hará el trabajo por usted.

Editar: asegúrese de que tiene alineación al crear el búfer en main.

Actualización:

Solo quería dar una actualización sobre las cosas que se discutieron en los comentarios.

  1. Lo primero que se mencionó fue que es posible que necesitemos actualizar el puntero flotante creado inicialmente al puntero devuelto por los flotadores reubicados-new'ed (la pregunta es si el puntero flotante inicial todavía se puede usar para acceder a los flotadores, porque los flotadores ahora son flotadores "nuevos" obtenidos por un nuevo expresion).

Para hacer esto, podemos a) pasar el puntero flotante por referencia y actualizarlo, o b) devolver el nuevo puntero flotante obtenido de la función:

A)

void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
    double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];    
    // do some work here on/with the doubles...
    buffer = new (buffer) float[10];  
}

B)

float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
    /* same as inital example... */
    return new (buffer) float[10];  
}

int main()
{
    float* floats = new float[10];
    floats = f(floats, sizeof(float) * 10);
    return 0;
}
  1. La siguiente y más crucial cosa a mencionar es que placement-new tiene permitido tener una sobrecarga de memoria. Por lo tanto, se permite a la implementación colocar algunos metadatos delante de la matriz devuelta. Si eso sucede, el cálculo ingenuo de cuántos dobles encajar en nuestra memoria será obviamente incorrecto. El problema es que no sabemos cuántos bytes adquirirá la implementación de antemano para la llamada específica. Pero eso sería nessessary ajustar las cantidades de dobles que sabemos cabrá en el almacenamiento restante. Aquí ( https://stackoverflow.com/a/8721932/3783662 ) es otro post de SO donde Howard Hinnant proporcionó un fragmento de prueba. Probé esto usando un compilador en línea y vi que para tipos destructables triviales (por ejemplo dobles), la sobrecarga era 0. Para tipos más complejos (por ejemplo std::string), había una sobrecarga de 8 bytes. Pero esto puede varry para su plataforma / compilador. Pruébelo de antemano con el fragmento de Howard.

  2. Para la pregunta por qué necesitamos usar algún tipo de colocación new (ya sea por new [] o single element new): Se nos permite lanzar punteros en todas las formas que queramos. Pero al final, cuando accedemos al valor, necesitamos usar el tipo correcto para evitar voilating el estricto reglas de aliasing. Easy speaking: solo se permite acceder a un objeto cuando realmente hay un objeto del tipo puntero viviendo en la ubicación dada por el puntero. Entonces, ¿cómo traes objetos a la vida? el estándar dice:

Https://timsong-cpp.github.io/cppwp/intro.object#1 :

"Un objeto es creado por una definición, por una nueva expresión, cuando cambia implícitamente el miembro activo de una unión, o cuando se crea un objeto temporal."

Hay un sector adicional que puede parecer interesante:

Https://timsong-cpp.github.io/cppwp/basic.life#1 :

"Se dice que un objeto tiene inicialización no vacua si es de tipo clase o agregado y él o uno de sus subobjetos es inicializado por un constructor que no sea un constructor por defecto trivial. La vida útil de un objeto de tipo T comienza cuando:

  • se obtiene el almacenamiento con la alineación y el tamaño adecuados para el tipo T, y
  • si el objeto tiene inicialización no vacua, su inicialización es completa "

Así que ahora podemos argumentar que debido a que los dobles son triviales, ¿necesitamos tomar alguna acción para traer los objetos triviales a la vida y cambiar los objetos vivos reales? Digo que sí, porque inicialmente obtuvimos almacenamiento para los flotadores, y acceder al almacenamiento a través de un doble puntero violaría el alias estricto. Así que necesitamos decirle al compilador que el tipo real ha cambiado. Todo este último punto 3 fue bastante controvertido, discutido. Puede formarse su propia opinión. Ahora tienes toda la información a mano.

 10
Author: phön,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-18 06:58:08

Puedes lograr esto de dos maneras.

Primero:

void set(float *buffer, size_t index, double value) {
    memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
    double v;
    memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
    return v;
}
void f(float *buffer) {
    // here, use set and get functions
}

Segundo: en Lugar de float *, necesita asignar una "abreviado" char[] búfer y la colocación de nuevo para poner flotadores o dobles, interior:

template <typename T>
void setType(char *buffer, size_t size) {
    for (size_t i=0; i<size/sizeof(T); i++) {
        new(buffer+i*sizeof(T)) T;
    }
}
// use it like this: setType<float>(buffer, sizeOfBuffer);

Luego use este accesor:

template <typename T>
T &get(char *buffer, size_t index) {
    return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;

Una tercera vía podría ser algo así como la respuesta de phön (ver mis comentarios bajo esa respuesta), desafortunadamente no puedo hacer una solución adecuada, debido a este problema.

 7
Author: geza,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-12 05:40:04

Tl; dr No alias punteros - en absoluto - a menos que le digas al compilador que vas a hacerlo en la línea de comandos.


La forma más fácil de hacer esto podría ser averiguar qué conmutador de compilador deshabilita el alias estricto y usarlo para los archivos fuente en cuestión.

Las necesidades deben, ¿eh?


Pensé en esto un poco más. A pesar de todas esas cosas sobre la colocación nueva, esta es la única manera segura.

¿Por qué?

Bueno, si tienes dos indicadores de diferentes tipos que apuntan a la misma dirección entonces usted ha aliased esa dirección y usted tiene una buena oportunidad de engañar al compilador. Y no importa cómo asignes valores a esos punteros. El compilador no va a recordar eso.

Así que esta es la única forma segura, y es por eso que necesitamos std::pun.

 1
Author: Paul Sanders,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-12 05:19:43

Aquí hay un enfoque alternativo que es menos aterrador.

Usted dice,

...una unión flotante/doble no es posible sin ella...la asignación de espacio adicional, que frustra el propósito y pasa a ser costoso en mi caso...

Así que solo tiene que cada objeto de unión contiene dos flotadores en lugar de uno.

static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
    double d;
    float f[2];
};

void f(double_or_floats* buffer)
{
    // Use buffer of doubles as scratch space.
    buffer[0].d = 1.0;
    // Done with the scratch space.  Start filling the buffer with floats.
    buffer[0].f[0] = 1.0f;
    buffer[0].f[1] = 2.0f;
}

Por supuesto, esto hace que la indexación sea más complicada, y el código de llamada tendrá que ser modificado. Pero no tiene gastos generales y es más obviamente correcto.

 1
Author: Maxpm,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-18 00:06:13

Este problema no se puede resolver en portable C++.

C++ es estricto cuando se trata de aliasing de puntero. Paradójicamente, esto le permite compilar en muchas plataformas (por ejemplo, donde, tal vez double los números se almacenan en lugares diferentes a float los números).

No hace falta decir que, si usted está luchando por el código portátil, entonces usted tendrá que recodificar lo que tiene. La segunda mejor cosa es ser pragmático, aceptar que funcionará en cualquier sistema de escritorio que me he encontrado; tal vez even static_assert en nombre / arquitectura del compilador.

 0
Author: Bathsheba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-12 07:12:11

Editar

Pensé en esto un poco más y no está garantizado que sea seguro por las razones que he agregado a mi respuesta original. Así que dejaré el código aquí como referencia, pero no recomiendo que lo uses.

En lugar de eso, haz lo que te sugiero arriba. Es una pena, me gusta bastante el código que escribí, a pesar de los votos negativos misteriosos (me golpea el infierno, pensé que hice un buen trabajo aquí).

Edit 2: (solo para completar, este post es ya muerto)

Esta solución solo funcionará para tipos primitivos y POD. Esto es deliberado, dado el alcance de la pregunta original.


Pensé en publicar una respuesta de seguimiento porque @phön ha encontrado una mejor solución diferente que yo y quería ordenarla un poco y agregar algunas ideas propias.

Por favor nota: Este es un post serio. Solo porque me sienta un poco alegre hoy, no significa que solo esté engañando alrededor.

En primer lugar, asignaría el buffer 'master' usando malloc(). Esto es porque:

  • Me devuelve un void *, que en realidad es el apropiado aquí. Verás por qué en un minuto.
  • Es marginalmente más eficiente (aunque este es un detalle).
  • Puedo controlar la alineación del búfer si necesito (para SSE, por ejemplo) con aligned_alloc.

No hay realmente una desventaja de esto. Si quiero manejarlo con un puntero inteligente, puedo utilice siempre un deleter personalizado.

Entonces, ¿por qué es eso void * tan maravilloso? Porque me impide hacer el tipo de cosas que phön hizo en su post, es decir, que estaba tentado a 'revertir' el uso del buffer a una matriz de float s y no creo que eso sea sabio.

Mejor, más bien, es ciertamente más limpio, usar la colocación new cada vez que desee tratar el búfer como una matriz de Foo y luego dejar que ese puntero salga silenciosamente del alcance cuando haya terminado con él. Sobrecarga es mínimo, sin duda para los tipos de vaina. De hecho, esperaría que cualquier compilador decente lo optimizara completamente en este caso, pero no lo he probado.

Así que usted debe, por supuesto, envolver todo esto en una clase, así que vamos a hacer eso. Entonces no necesitamos ese deleter personalizado. Aquí va.

La clase:

#include <cstdlib>
#include <new>
#include <iostream>

class SneakyBuf
{
public:
    SneakyBuf (size_t bufsize, size_t alignment = 8) : m_bufsize (bufsize)
    {
        m_buf = aligned_alloc (alignment, bufsize);
        if (m_buf == nullptr)
            throw std::bad_alloc ();
        std::cout << std::hex << "m_buf is at " << m_buf << "\n\n";
    }

    ~SneakyBuf () { free (m_buf); }

    template <class T> T* Cast (size_t& count)
    {
        count = m_bufsize / sizeof (T);
        return new (m_buf) T;   // no need for new [] here
    }

private:
    size_t m_bufsize;
    void *m_buf;
};

Programa de prueba:

void do_float_stuff (SneakyBuf& sb)
{
    size_t count;
    float *f = sb.Cast <float> (count);
    std::cout << std::hex << "floats are at " << f << "\n";
    std::cout << std::dec << "We have " << count << " floats\n\n";
    f [0] = 0;
    // ...
}

void do_double_stuff (SneakyBuf& sb)
{
    size_t count;
    double *d = sb.Cast <double> (count);
    std::cout << std::hex << "doubles are at " << d << "\n";
    std::cout << std::dec << "We have " << count << " doubles\n";
    d [0] = 0;
    // ...
}

int main ()
{
    SneakyBuf sb (100 * sizeof (double));
    do_float_stuff (sb); 
    do_double_stuff (sb);
}

Salida:

m_buf is at 0x1e56c40

floats are at 0x1e56c40
We have 200 floats

doubles are at 0x1e56c40
We have 100 doubles

Demostración en Vivo.

Escrito en mi tabla, trabajo duro!

 -3
Author: Paul Sanders,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-13 06:05:52