¿Cómo determinar si la memoria está alineada?

10 minutos de lectura

avatar de usuario
usuario229898

Soy nuevo en la optimización de código con instrucciones SSE/SSE2 y hasta ahora no he llegado muy lejos. Que yo sepa, una función común optimizada para SSE se vería así:

void sse_func(const float* const ptr, int len){
    if( ptr is aligned )
    {
        for( ... ){
            // unroll loop by 4 or 2 elements
        }
        for( ....){
            // handle the rest
            // (non-optimized code)
        }
    } else {
        for( ....){
            // regular C code to handle non-aligned memory
        }
    }
}

Sin embargo, ¿cómo puedo determinar correctamente si la memoria ptr apunta a está alineado por ejemplo, 16 Bytes? Creo que tengo que incluir la ruta del código C normal para la memoria no alineada, ya que no puedo asegurarme de que todas las memorias pasadas a esta función estén alineadas. Y usar los intrínsecos para cargar datos de la memoria no alineada en los registros SSE parece ser terriblemente lento (incluso más lento que el código C normal).

Gracias de antemano…

  • nombre aleatorio, no estoy seguro, pero creo que podría ser más eficiente simplemente manejar los primeros elementos ‘no alineados’ por separado como lo hace con los últimos. Entonces aún puede usar SSE para los ‘medios’ …

    – Rehno Lindeque

    21 de diciembre de 2009 a las 12:27

  • Hm, este es un buen punto. Lo intentaré. ¡Gracias!

    – usuario229898

    22 de diciembre de 2009 a las 16:15

  • Mejor: use un prólogo escalar para manejar los elementos desalineados hasta el primer límite de alineación. (gcc hace esto cuando se vectoriza automáticamente con un puntero de alineación desconocida). O si su algoritmo es idempotente (como a[i] = foo(b[i])), haga un primer vector potencialmente desalineado, luego el bucle principal que comienza en el primer límite de alineación después del primer vector, luego un vector final que termina en el último elemento. Si la matriz de hecho estaba desalineada y/o el conteo no era un múltiplo del ancho del vector, entonces algunos de esos vectores se superpondrán, pero eso aún supera al escalar.

    – Peter Cordes

    23 de agosto de 2017 a las 13:50

  • Lo mejor: proporcione un asignador que proporcione una memoria alineada de 16 bytes. Luego opere en el búfer alineado de 16 bytes sin la necesidad de corregir los elementos principales o secundarios. Esto es lo que hacen las bibliotecas como Botan y Crypto++ para los algoritmos que usan SSE, Altivec y sus amigos.

    – jww

    24 de agosto de 2018 a las 14:10


avatar de usuario
Cristóbal

#define is_aligned(POINTER, BYTE_COUNT) \
    (((uintptr_t)(const void *)(POINTER)) % (BYTE_COUNT) == 0)

el elenco a void * (o, equivalentemente, char *) es necesario porque el estándar solo garantiza una conversión invertible a uintptr_t por void *.

Si desea escribir seguridad, considere usar una función en línea:

static inline _Bool is_aligned(const void *restrict pointer, size_t byte_count)
{ return (uintptr_t)pointer % byte_count == 0; }

y espero optimizaciones del compilador si byte_count es una constante de tiempo de compilación.

¿Por qué tenemos que convertir a void * ?

El lenguaje C permite diferentes representaciones para diferentes tipos de punteros, por ejemplo, podría tener un puntero de 64 bits void * tipo (todo el espacio de direcciones) y una de 32 bits foo * tipo (un segmento).

La conversión foo * -> void * podría implicar un cálculo real, por ejemplo, agregar una compensación. El estándar también deja en manos de la implementación lo que sucede al convertir punteros (arbitrarios) en números enteros, pero sospecho que a menudo se implementa como un noop.

Para tal implementación, foo * -> uintptr_t -> foo * funcionaría, pero foo * -> uintptr_t -> void * y void * -> uintptr_t -> foo * no lo haría El cálculo de la alineación tampoco funcionaría de manera confiable porque solo verifica la alineación en relación con el desplazamiento del segmento, que podría o no ser lo que desea.

En conclusión: Utilice siempre void * para obtener un comportamiento independiente de la implementación.

  • Esta macro se ve realmente desagradable y sofisticada a la vez. Definitivamente lo probaré.

    – usuario229898

    14 de diciembre de 2009 a las 17:06

  • Proporcione cualquier ejemplo que conozca de plataformas en las que non-void * no produce un valor entero en el rango de uintptr_t. Y/o, ¿sabe cuál es la justificación para que el estándar esté redactado de esa manera?

    –Craig McQueen

    25 de noviembre de 2010 a las 23:07

  • ¿Por qué restringir? Parece que no hace nada cuando solo hay un puntero.

    – Mijaíl

    23 de septiembre de 2015 a las 6:45


  • @Mikhail: la combinación de const * con restrict es una garantía más fuerte que la simple const *: sin restrictes lícito desechar el const y modificar la memoria; con restrict presente, no lo es; lamentablemente, aprendí que esto no es útil en la práctica, ya que solo entra en vigencia si el puntero se usa realmente, lo que la persona que llama no puede asumir en general (es decir, la utilidad reside únicamente en el lado de la persona que llama); en este caso particular, es superfluo de todos modos ya que estamos tratando con una función en línea, por lo que el compilador puede ver su cuerpo e inferir por sí mismo que no se modifica la memoria

    – Cristóbal

    23/09/2015 a las 16:52


  • si un float * puede (teóricamente) tener una representación diferente de un void *¿eso significa que la verificación de alineación podría estar ocurriendo en un valor diferente al previsto?

    – mwfearnley

    13 de marzo de 2019 a las 21:07

avatar de usuario
Pascal Cuoq

EDITAR: enviar a long es una forma económica de protegerse contra la posibilidad más probable de que int y punteros sean de diferentes tamaños hoy en día.

Como se señala en los comentarios a continuación, hay mejores soluciones si está dispuesto a incluir un encabezado…

un puntero p está alineado en un límite de 16 bytes iff ((unsigned long)p & 15) == 0.

  • En su lugar, podrías usar uintptr_t – se garantiza el tamaño correcto para sujetar un puntero. Siempre que su compilador lo defina, por supuesto.

    – Anónimo.

    13 de diciembre de 2009 a las 23:26

  • Realmente no importa si los tamaños del puntero y del entero no coinciden. Solo te importan los últimos bits.

    –Richard Pennington

    13 de diciembre de 2009 a las 23:29

  • normalmente usaría p % 16 == 0ya que los compiladores generalmente conocen los poderes de 2 tan bien como yo, y encuentro esto más legible

    – Hasturkun

    13 de diciembre de 2009 a las 23:30

  • @Hasturkun División/módulo sobre enteros firmados no se compilan en trucos bit a bit en C99 (algunas cosas estúpidas de redondeo hacia cero), y es un compilador inteligente que reconocerá que el resultado del módulo se compara con cero (en el que caso de que las cosas bit a bit funcionen de nuevo). No imposible, pero no trivial. En términos generales, es mejor convertir a un entero sin signo si desea usar % y dejar que el compilador compile &.

    – Pascal Cuoq

    13 de diciembre de 2009 a las 23:34

  • @Pascal Cuoq, gcc nota esto y emite exactamente el mismo código para (p & 15) == 0 y (p % 16) == 0 con el -O conjunto de banderas He visto una serie de otros compiladores que reconocen la división/módulo/multiplicación de enteros por una potencia de 2 y hacen lo inteligente al respecto. (Sin embargo, estoy de acuerdo con enviar a unsigned)

    – Hasturkun

    13 de diciembre de 2009 a las 23:43

Otras respuestas sugieren una operación AND con bits bajos establecidos y comparando con cero.

Pero una prueba más directa sería hacer un MOD con el valor de alineación deseado y compararlo con cero.

#define ALIGNMENT_VALUE     16u

if (((uintptr_t)ptr % ALIGNMENT_VALUE) == 0)
{
    // ptr is aligned
}

  • Te voté a favor, pero solo porque estás usando números enteros sin signo 🙂

    – Pascal Cuoq

    13 de diciembre de 2009 a las 23:36

  • Creo que esto falla con uint8_t tipos, que a veces tienen requisitos de alineación de 1.

    – jww

    24 de agosto de 2018 a las 14:07

  • @jww No estoy seguro de entender lo que quieres decir. Un requisito de alineación de 1 significaría esencialmente ningún requisito de alineación. No hay necesidad de preocuparse por la alineación de uint8_t. Pero por favor aclare si estoy malinterpretando.

    –Craig McQueen

    29 de agosto de 2018 a las 12:13

  • los u sufijo en el número entero hace que no esté firmado. Es bueno evitar mezclar expresiones firmadas y no firmadas, para evitar algunos errores posibles que pueden ocurrir con la aritmética de signos mixtos. Consulte la advertencia de GCC “comparación entre expresiones enteras con signo y sin signo”. Probablemente no importe en este caso, pero es bueno adquirir buenos hábitos. (supongo que el 0 debiera ser 0u también)

    –Craig McQueen

    8 de agosto de 2019 a las 5:10


  • Tenga en cuenta que no debe usar una operación MOD real, es una operación bastante costosa y debe evitarse tanto como sea posible. Siempre debe utilizar la operación and. Pero creo que si tiene un compilador lo suficientemente sofisticado con todas las opciones de optimización habilitadas, convertirá automáticamente su operación MOD en un solo código de operación. (Usos del kernel de Linux y operación también para tu información)

    – rez

    11 de septiembre de 2021 a las 8:59


avatar de usuario
Rubiks

Con una plantilla de función como

#include <type_traits>

template< typename T >
bool is_aligned(T* p){
    return !(reinterpret_cast<uintptr_t>(p) % std::alignment_of<T>::value);
}

podría verificar la alineación en tiempo de ejecución invocando algo como

struct foo_type{ int bar; }foo;
assert(is_aligned(&foo)); // passes

Para verificar que fallan las malas alineaciones, podría hacer

// would almost certainly fail
assert(is_aligned((foo_type*)(1 + (uintptr_t)(&foo)));

Esto es básicamente lo que estoy usando. Al convertir el número entero en una plantilla, me aseguro de que se amplíe el tiempo de compilación, por lo que no terminaré con una operación de módulo lenta, haga lo que haga.

Siempre me gusta verificar mi entrada, por lo tanto, la afirmación del tiempo de compilación. Si su valor de alineación es incorrecto, bueno, entonces no se compilará …

template <unsigned int alignment>
struct IsAligned
{
    static_assert((alignment & (alignment - 1)) == 0, "Alignment must be a power of 2");

    static inline bool Value(const void * ptr)
    {
        return (((uintptr_t)ptr) & (alignment - 1)) == 0;
    }
};

Para ver lo que está pasando, puedes usar esto:

// 1 of them is aligned...
int* ptr = new int[8];
for (int i = 0; i < 8; ++i)
    std::cout << IsAligned<32>::Value(ptr + i) << std::endl;

// Should give '1'
int* ptr2 = (int*)_aligned_malloc(32, 32);
std::cout << IsAligned<32>::Value(ptr2) << std::endl;

avatar de usuario
alfc

Deja eso a los profesionales,

https://www.boost.org/doc/libs/1_65_1/doc/html/align/reference.html#align.reference.functions.is_aligned

bool is_aligned(const void* ptr, std::size_t alignment) noexcept; 

ejemplo:

        char D[1];
        assert( boost::alignment::is_aligned(&D[0], alignof(double)) ); //  might fail, sometimes

avatar de usuario
pablo tomblin

¿Puede simplemente ‘y’ el ptr con 0x03 (alineado en 4s), 0x07 (alineado en 8s) o 0x0f (alineado en 16s) para ver si alguno de los bits más bajos está configurado?

  • No, no puedes. Un puntero no es un argumento válido para el operador &.

    –Steve Jessop

    13 de diciembre de 2009 a las 23:34

  • @SteveJessop podrías enviar a uintptr_t.

    usuario6754053

    20 de diciembre de 2016 a las 23:10

  • @MarkYisri: sí, espero que en la práctica, cada implementación que admita las instrucciones SSE2 proporcione una garantía específica de implementación que funcionará 🙂

    –Steve Jessop

    10 de enero de 2017 a las 11:42


¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad