ejercicios resueltos punteros c++
¿Cuál es la forma recomendada de alinear la memoria en C++ 11? (4)
Estoy trabajando en una única implementación de buffer de anillo de consumidor de un solo productor. Tengo dos requisitos:
1) Alinear una única instancia asignada de almacenamiento dinámico de un anillo a una línea de caché.
2) Alinee un campo dentro de un búfer de anillo con una línea de caché (para evitar compartir falsamente).
Mi clase se ve algo así como:
#define CACHE_LINE_SIZE 64 // To be used later.
template<typename T, uint64_t num_events>
class RingBuffer { // This needs to be aligned to a cache line.
public:
....
private:
std::atomic<int64_t> publisher_sequence_ ;
int64_t cached_consumer_sequence_;
T* events_;
std::atomic<int64_t> consumer_sequence_; // This needs to be aligned to a cache line.
};
Permítanme primero abordar el punto 1, es decir, alinear una única instancia de la clase asignada en el montón . Hay algunas maneras:
1) Use el especificador de c ++ 11 alignas(..)
:
template<typename T, uint64_t num_events>
class alignas(CACHE_LINE_SIZE) RingBuffer {
public:
....
private:
// All the private fields.
};
2) Use posix_memalign(..)
+ placement new(..)
sin alterar la definición de clase. Esto no es independiente de la plataforma:
void* buffer;
if (posix_memalign(&buffer, 64, sizeof(processor::RingBuffer<int, kRingBufferSize>)) != 0) {
perror("posix_memalign did not work!");
abort();
}
// Use placement new on a cache aligned buffer.
auto ring_buffer = new(buffer) processor::RingBuffer<int, kRingBufferSize>();
3) Use la extensión GCC / Clang __attribute__ ((aligned(#)))
template<typename T, uint64_t num_events>
class RingBuffer {
public:
....
private:
// All the private fields.
} __attribute__ ((aligned(CACHE_LINE_SIZE)));
4) Intenté usar la función C ++ 11 aligned_alloc(..)
estandarizada aligned_alloc(..)
lugar de posix_memalign(..)
pero GCC 4.8.1 en Ubuntu 12.04 no pudo encontrar la definición en stdlib.h
¿Están todos garantizados para hacer lo mismo? Mi objetivo es la alineación de la línea de caché, por lo que cualquier método que tenga algunos límites en la alineación (digamos doble palabra) no funcionará. La independencia de la plataforma que apuntaría a usar las alignas(..)
estandarizadas alignas(..)
es un objetivo secundario.
No tengo claro si las alignas(..)
y __attribute__((aligned(#)))
tienen algún límite que podría estar debajo de la línea de caché en la máquina. No puedo reproducir esto más, pero al imprimir direcciones, creo que no siempre alignas(..)
direcciones alineadas de 64 bytes con alignas(..)
. Por el contrario, posix_memalign(..)
parecía funcionar siempre. De nuevo, no puedo reproducir esto más, así que tal vez estaba cometiendo un error.
El segundo objetivo es alinear un campo dentro de una clase / estructura con una línea de caché. Estoy haciendo esto para evitar el intercambio falso. He intentado de las siguientes maneras:
1) Use el especificador de C ++ 11 alignas(..)
:
template<typename T, uint64_t num_events>
class RingBuffer { // This needs to be aligned to a cache line.
public:
...
private:
std::atomic<int64_t> publisher_sequence_ ;
int64_t cached_consumer_sequence_;
T* events_;
std::atomic<int64_t> consumer_sequence_ alignas(CACHE_LINE_SIZE);
};
2) Use la extensión GCC / Clang __attribute__ ((aligned(#)))
template<typename T, uint64_t num_events>
class RingBuffer { // This needs to be aligned to a cache line.
public:
...
private:
std::atomic<int64_t> publisher_sequence_ ;
int64_t cached_consumer_sequence_;
T* events_;
std::atomic<int64_t> consumer_sequence_ __attribute__ ((aligned (CACHE_LINE_SIZE)));
};
Ambos métodos parecen alinear consumer_sequence
a una dirección de 64 bytes después del comienzo del objeto, por lo que si consumer_sequence
está alineado en caché depende de si el objeto en sí está alineado en caché. Aquí mi pregunta es: ¿hay alguna forma mejor de hacer lo mismo?
EDITAR: El motivo por el que alignment_alloc no funcionó en mi máquina fue porque estaba en eglibc 2.15 (Ubuntu 12.04). Funcionó en una versión posterior de eglibc.
Desde la página man : The function aligned_alloc() was added to glibc in version 2.16
.
Esto me hace bastante inútil ya que no puedo requerir una versión tan reciente de eglibc / glibc.
Desafortunadamente, lo mejor que he encontrado es asignar espacio extra y luego usar la parte "alineada". Entonces, el RingBuffer new
puede solicitar 64 bytes adicionales y luego devolver la primera parte alineada de 64 bytes. Desperdicia espacio pero te dará la alineación que necesitas. Es probable que deba configurar la memoria antes de lo que se devuelve a la dirección de asignación real para desasignarla.
[Memory returned][ptr to start of memory][aligned memory][extra memory]
(suponiendo que no hay herencia de RingBuffer) algo así como:
void * RingBuffer::operator new(size_t request)
{
static const size_t ptr_alloc = sizeof(void *);
static const size_t align_size = 64;
static const size_t request_size = sizeof(RingBuffer)+align_size;
static const size_t needed = ptr_alloc+request_size;
void * alloc = ::operator new(needed);
void *ptr = std::align(align_size, sizeof(RingBuffer),
alloc+ptr_alloc, request_size);
((void **)ptr)[-1] = alloc; // save for delete calls to use
return ptr;
}
void RingBuffer::operator delete(void * ptr)
{
if (ptr) // 0 is valid, but a noop, so prevent passing negative memory
{
void * alloc = ((void **)ptr)[-1];
::operator delete (alloc);
}
}
Para el segundo requisito de tener un miembro de datos de RingBuffer
también alineado con 64 bytes, para que si sabes que el inicio de this
está alineado, puedes rellenar para forzar la alineación para los miembros de datos.
Después de investigar un poco mis pensamientos son:
1) Al igual que @TemplateRex señaló que no parece haber una forma estándar de alinearse a más de 16 bytes. Entonces, incluso si usamos las alignas(..)
estandarizadas alignas(..)
no hay garantía a menos que el límite de alineación sea menor o igual a 16 bytes. Tendré que verificar que funcione como se espera en una plataforma de destino.
2) __attribute ((aligned(#)))
o alignas(..)
no se puede usar para alinear un objeto alignas(..)
como he sospechado, es decir, new()
no hace nada con estas anotaciones. Parecen funcionar para objetos estáticos o asignaciones de pila con las advertencias de (1).
Ya sea posix_memalign(..)
(no estándar) o aligned_alloc(..)
(estandarizado pero no pudo hacerlo funcionar en GCC 4.8.1) + la ubicación new(..)
parece ser la solución. Mi solución para cuando necesito código independiente de la plataforma es macros específicas del compilador :)
3) La alineación para los campos struct / class parece funcionar tanto con __attribute ((aligned(#)))
como con alignas()
como se indica en la respuesta. Una vez más, creo que las advertencias de (1) sobre las garantías en la alineación se mantienen.
Así que mi solución actual es usar posix_memalign(..)
+ placement new(..)
para alinear una instancia de mi clase asignada en heap ya que mi plataforma de destino ahora solo es Linux. También estoy usando alignas(..)
para alinear campos ya que está estandarizado y al menos funciona en Clang y GCC. Estaré encantado de cambiarlo si aparece una mejor respuesta.
La respuesta a su problema es std::aligned_storage . Se puede usar en el nivel superior y para miembros individuales de una clase.
No sé si es la mejor manera de alinear la memoria asignada con un nuevo operador, ¡pero sin duda es muy simple!
Esta es la forma en que se hace en el pase de sanitización de hilo en GCC 6.1.0
#define ALIGNED(x) __attribute__((aligned(x)))
static char myarray[sizeof(myClass)] ALIGNED(64) ;
var = new(myarray) myClass;
Bueno, en sanitizer_common / sanitizer_internal_defs.h, también está escrito
// Please only use the ALIGNED macro before the type.
// Using ALIGNED after the variable declaration is not portable!
Así que no sé por qué el ALINEADO aquí se usa después de la declaración de la variable. Pero es otra historia.