Почему ICU использует этот барьер псевдонимов при выполнении reinterpret_cast?

Я переношу код из ICU 58.2 в ICU 59.1, где они изменили тип символа с uint16_t на char16_t. Я собирался просто сделать прямое reinterpret_cast, где мне нужно было преобразовать типы, но обнаружил, что ICU 59.1 на самом деле предоставляет функции для этого преобразования. Чего я не понимаю, так это того, почему им нужно использовать этот барьер сглаживания перед выполнением reinterpret_cast.

#elif (defined(__clang__) || defined(__GNUC__)) && U_PLATFORM != 
U_PF_BROWSER_NATIVE_CLIENT
#   define U_ALIASING_BARRIER(ptr) asm volatile("" : : "rm"(ptr) : "memory")
#endif

...

    inline const UChar *toUCharPtr(const char16_t *p) {
#ifdef U_ALIASING_BARRIER
    U_ALIASING_BARRIER(p);
#endif
    return reinterpret_cast<const UChar *>(p);

Почему нельзя просто использовать reinterpret_cast без вызова U_ALIASING_BARRIER?


person dennisbk    schedule 19.09.2017    source источник


Ответы (1)


Предположительно, это должно остановить любые нарушения строгого правила псевдонимов, что может возникнуть при вызове кода, который не был полностью очищен, из-за неожиданного поведения при оптимизации (подсказка к этому находится в комментарии выше: «Барьер для оптимизации сглаживания указателя даже через границы функций»).

Строгие правила псевдонимов запрещают разыменование указателей, которые используют псевдонимы для одного и того же значения, когда они имеют несовместимые типы (понятие C, но C++ говорит то же самое с большим количеством слов). Вот небольшая загвоздка: char16_t и uint16_t не обязательно должны быть совместимы. uint16_t на самом деле является опционально поддерживаемым типом (как в C, так и в C++); char16_t эквивалентен uint_least16_t, который не обязательно того же типа. Он будет иметь одинаковую ширину на x86, но компилятору не требуется помечать его как на самом деле одно и то же. Это может быть даже намеренно нестрогим с предполагаемыми типами, которые обычно указывают другое намерение, может быть псевдонимом.

В связанном ответе есть более полное объяснение, но в основном это такой код:

uint16_t buffer[] = ...

buffer[0] = u'a';
uint16_t * pc1 = buffer;

char16_t * pc2 = (char16_t *)pc1;
pc2[0] = u'b';

uint16_t c3 = pc1[0];

... если по какой-либо причине компилятор не пометил char16_t и uint16_t как совместимые, и вы компилируете с оптимизацией, включающей эквивалент -fstrict-aliasing, можно предположить, что запись через pc2 не могла изменить что-либо pc1 указывает на и не перезагружать значение, прежде чем присвоить его c3, вместо этого, возможно, присвоив ему u'a'.

Код, немного похожий на пример, мог бы появиться в середине процесса преобразования, где предыдущий код успешно использовал uint16_t * везде, но теперь char16_t * доступен в верхней части блока для совместимости с ICU 59, перед всем кодом ниже. был полностью изменен, чтобы читать только через правильно типизированный указатель.

Поскольку компиляторы обычно не оптимизируют ручную сборку, наличие блока asm заставит его проверить все свои предположения о состоянии регистров и других временных значений и выполнить полную перезагрузку каждого значения при первом разыменовании. после U_ALIASING_BARRIER вне зависимости от флагов оптимизации. Это не защитит вас от каких-либо дальнейших проблем с псевдонимами, если вы продолжите записывать через uint16_t * ниже преобразования (если вы это сделаете, это законная ваша вина), но это должно, по крайней мере, обеспечить состояние до того, как вызов преобразования не сохраняется таким образом, что впоследствии может быть случайно пропущена запись через новый указатель.

person Leushenko    schedule 20.09.2017
comment
Хорошо объяснил. Я так понимаю сам. (команда СИС) - person Steven R. Loomis; 01.10.2017