Я борюсь с реализацией буфера совместно используемой памяти без нарушения строгих правил алиасинга в C99.
Предположим, у меня есть некоторый код, который обрабатывает некоторые данные и должен иметь «чистую» память для работы. Я мог бы написать это как-то так:
void foo(... some arguments here ...) {
int* scratchMem = new int[1000]; // Allocate.
// Do stuff...
delete[] scratchMem; // Free.
}
Затем у меня есть другая функция, которая делает некоторые другие вещи, которые также нуждаются в буфере очистки:
void bar(...arguments...) {
float* scratchMem = new float[1000]; // Allocate.
// Do other stuff...
delete[] scratchMem; // Free.
}
Проблема заключается в том, что foo () и bar () могут вызываться много раз во время работы, и распределение ресурсов кучи повсеместно может быть довольно плохим с точки зрения производительности и фрагментации памяти. Очевидным решением было бы выделить общий буфер общей памяти надлежащего размера один раз, а затем передать его в foo () и bar () в качестве аргумента в стиле BYOB:
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!
// ...
}
Я думаю, у меня есть два вопроса сейчас:
— Как я могу реализовать общий буфер с общей буферной памятью, который не нарушает правила псевдонимов?
— Несмотря на то, что приведенный выше код нарушает строгие правила псевдонимов, «псевдоним» не наносит «вреда». Поэтому может ли какой-нибудь здравомыслящий компилятор генерировать (оптимизированный) код, который все еще доставляет мне неприятности?
Спасибо
На самом деле, то, что вы написали, не является строгим нарушением псевдонимов.
C ++ 11 spec 3.10.10 говорит:
Если программа пытается получить доступ к сохраненному значению объекта через glvalue, отличный от одного из
следующие типы поведение не определено
Таким образом, вещь, которая вызывает неопределенное поведение, это доступ к сохраненному значению, а не просто создание указателя на него. Ваш пример ничего не нарушает. Это должно было бы сделать следующий шаг: float badValue = smem [0]. Smem [0] получает сохраненное значение из общего буфера, создавая нарушение псевдонимов.
Конечно, вы не собираетесь просто захватывать smem [0] перед установкой. Вы собираетесь написать в первую очередь. Присвоение одной и той же памяти не дает доступа к сохраненному значению, поэтому нет необходимости в использовании. Однако, нельзя писать поверх объекта, пока он еще жив. Чтобы доказать, что мы в безопасности, нам нужна продолжительность жизни объекта из 3.8.4:
Программа может закончить время жизни любого объекта, повторно используя хранилище, которое занимает объект, или
явный вызов деструктора для объекта типа класса с нетривиальным деструктором. Для объекта
типа класса с нетривиальным деструктором, программе не требуется явно вызывать деструктор
до повторного использования или освобождения хранилища, которое занимает объект; … [продолжает о последствиях отказа от вызова деструкторов]
У вас есть POD-тип, такой тривиальный деструктор, так что вы можете просто объявить в устной форме «все объекты int находятся в конце своей жизни, я использую пространство для float». Затем вы повторно используете пространство для поплавков, и нарушения псевдонимов не происходит.
Всегда допустимо интерпретировать объект как последовательность байтов (т.е. не нарушение псевдонимов для обработки любого указателя объекта как указателя на первый элемент массива символов), и вы можете создать объект в любой части памяти, которая достаточно велика и выровнена соответствующим образом.
Таким образом, вы можете выделить большой массив char
s (любая подпись) и найдите смещение, которое установлено на alignof(maxalign_t)
; теперь вы можете интерпретировать этот указатель как указатель на объект после того, как вы сконструировали соответствующий объект (например, с помощью размещения-нового в C ++).
Вы, конечно, должны убедиться, что не записываете в память существующего объекта; на самом деле время жизни объекта тесно связано с тем, что происходит с памятью, которая представляет объект.
Пример:
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();
}
Обратите внимание, что память, полученная malloc
или же new char[N]
является всегда выровнен для максимального выравнивания (но не больше, и вы можете использовать переопределенные адреса).
Если для хранения переменных int и float используется объединение, то вы можете пропустить строгий псевдоним. Подробнее об этом дается в
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
Также см. Следующую статью.
http://blog.regehr.org/archives/959
Он дает способ использовать союзы, чтобы сделать это.