У меня есть функция, которая по существу создает хэш-значение для произвольной области памяти. Входной аргумент использует тип const void*
, как способ сказать «это может быть что угодно». Итак по существу:
unsigned hash(const void* ptr, size_t size);
Все идет нормально.
Блоб байтов может быть чем угодно, и его начальный адрес может быть где угодно. Это означает, что иногда он выровнен по 32-битным границам, а иногда нет.
На некоторых платформах (например, armv6
или mips
) чтение из невыровненной памяти приводит к значительному снижению производительности. На самом деле невозможно напрямую прочитать 32-битные данные из невыровненной памяти, поэтому компилятор предпочитает более безопасный алгоритм побайтовой рекомбинации (точные детали реализации скрыты за memcpy()
).
Метод безопасного доступа, конечно, намного медленнее, чем прямой 32-битный доступ, который сам по себе возможен только в том случае, если входные данные правильно выровнены по 32-битной границе. Это приводит к тому, что дизайн пытается разделить 2 случая: когда ввод не выровнен, используйте безопасный путь кода доступа, когда ввод выровнен (эффективно довольно часто), используйте прямой путь 32-битного кода доступа.
Разница в производительности огромна, мы не говорим здесь о нескольких процентах, это означает увеличение производительности в 5 раз, а иногда и больше. Так что это не просто «хорошо», это на самом деле делает функцию конкурентоспособной или нет, полезной или нет.
Этот дизайн до сих пор работал нормально в приличном количестве сценариев.
Введите инлайнинг.
Теперь, когда реализация функции доступна во время компиляции, умный компилятор может убрать все уровни косвенности и сократить реализацию до основных элементов. В случае, когда он может доказать, что ввод обязательно выровнен, например, struct
с определенными элементами, он может упростить код, удалить все косвенные const void*
и перейти к базовой реализации, где область памяти эффективно читается с использованием const u32*
указатель.
И теперь может вступить в действие строгий псевдоним, так как область ввода записывается с использованием struct S*
, а считывается с использованием другого const u32*
, что позволяет компилятору рассматривать эти две операции как полностью независимые, в конечном итоге переупорядочивая их, что приводит к неверный исход.
По сути, это интерпретация, которую я получил от пользователя. Возможно, стоит отметить, что мне не удалось воспроизвести проблему, но проблемы со строгим псевдонимом, обнаруженные с помощью встраивания, это известная тема. Также известно, что строгое сглаживание может быть трудно воспроизвести из-за крошечных деталей реализации, что приводит к различным вариантам оптимизации в зависимости от версии компилятора. Поэтому я считаю отчет заслуживающим доверия, но не могу изучить его напрямую из-за отсутствия случая репродукции.
Во всяком случае, теперь возникает вопрос. Как правильно поступить в этом случае? "Безопасное" решение состоит в том, чтобы всегда использовать путь memcpy()
, но это настолько снижает производительность, что делает функцию просто бесполезной. Кроме того, это, очевидно, ужасная трата энергии. Простой выход - не встраиваться, хотя это приводит к собственным накладным расходам на вызов функции (честно говоря, не таким уж большим) и, что более важно, просто "скрывает" проблему, а не решает ее.
Но я еще не нашел решения для него. Мне сказали, что независимо от того, какой тип промежуточного указателя используется, даже если const char*
является частью цепочки приведения, это не помешает окончательной операции чтения const u32*
нарушить строгое сглаживание (просто повторяю, я не могу проверить это, потому что я не могу воспроизвести случай). Описанное таким образом, это кажется почти безнадежным.
Но я не могу не отметить, что memcpy()
может должным образом избежать такого риска переупорядочения, хотя его интерфейс также использует const void*
, и точная реализация сильно различается, но мы можем быть уверены, что это не просто чтение побайтно. байт const char*
, так как производительность превосходна, и без колебаний использует векторный код, когда он быстрее. Кроме того, memcpy()
— это функция, которая определенно много встроена. Поэтому я думаю, что должно быть решение этой проблемы.
__attribute__((may_alias))
. - person Nate Eldredge   schedule 05.06.2020unsigned
будет таким же, какuint32_t
, или вам нужна переносимость? - person chux - Reinstate Monica   schedule 05.06.2020__attribute__((may_alias))
является лучшим ответом, поскольку пока не найдено портативного решения (помимо использованияmemcpy()
, снижающего производительность в некоторых сценариях, как описано выше). - person Cyan   schedule 20.06.2020