Указатели на статические переменные должны уважать каноническую форму? - PullRequest
0 голосов
/ 30 апреля 2019

Предполагая, что у меня есть следующий пример:

struct Dummy {
    uint64_t m{0llu};

    template < class T > static uint64_t UniqueID() noexcept {
        static const uint64_t uid = 0xBEA57;
        return reinterpret_cast< uint64_t >(&uid);
    }
    template < class T > static uint64_t BuildID() noexcept {
        static const uint64_t id = UniqueID< T >()
               // dummy bits for the sake of example (whole last byte is used)
               | (1llu << 60llu) | (1llu << 61llu) | (1llu << 63llu);
        return id;
    }
    // Copy bits 48 through 55 over to bits 56 through 63 to keep canonical form.
    uint64_t GetUID() const noexcept {
        return ((m & ~(0xFFllu << 56llu)) | ((m & (0xFFllu << 48llu)) << 8llu));
    }
    uint64_t GetPayload() const noexcept {
        return *reinterpret_cast< uint64_t * >(GetUID());
    }
};

template < class T > inline Dummy DummyID() noexcept {
    return Dummy{Dummy::BuildID< T >()};
}

Очень хорошо зная, что результирующий указатель является адресом статической переменной в программе.

Когда я вызываю GetUID() doМне нужно убедиться, что бит 47 повторяется до бита 63?

Или я могу просто И с маской из младших 48 бит и игнорировать это правило.

Я не смог найти ни одногоинформация об этом.И я предполагаю, что эти 16 битов, вероятно, всегда будут 0.

Этот пример строго ограничен архитектурой x86_64 (x32).

1 Ответ

2 голосов
/ 30 апреля 2019

В коде пользовательского пространства для основных операционных систем x86-64 обычно можно предположить, что старшие биты любого допустимого адреса равны нулю.

AFAIK, все основные операционные системы x86-64 используют дизайн high-half kernel , где адреса пространства пользователя всегда находятся в нижнем каноническом диапазоне.

Если вы хотите, чтобы этот код работал и в коде ядра, вам нужно подписать расширение с помощью x <<= 16; x >>= 16;, используя подписанное int64_t x.


Если компилятор не может хранить 0x0000FFFFFFFFFFFF = (1ULL<<48)-1 в регистре при многократном использовании, 2 смены могут быть более эффективными в любом случае. (mov r64, imm64 для создания этой широкой константы - это 10-байтовая инструкция, которая иногда может быть медленной для декодирования или извлечения из кэша UOP.) Но если вы компилируете с -march=haswell или новее, то BMI1 доступен, поэтому компилятор можно сделать mov eax, 48 / bzhi rsi, rdi, rax. В любом случае, один AND или BZHI - это только 1 цикл задержки критического пути для указателя против 2 для 2 смен. К сожалению, BZHI не доступен с непосредственным операндом. (Инструкции для битового поля x86 в большинстве своем отстой по сравнению с ARM или PowerPC.)

Ваш текущий метод извлечения битов [55:48] и использования их для замены текущих битов [63:56], вероятно, медленнее , потому что компилятор должен маскировать старый старший байт, а затем ИЛИ в новом старшем байт. Это уже минимум 2 цикла задержки, так что вы можете просто сдвинуть или замаскировать, что может быть быстрее.

В x86 есть дерьмовые инструкции для битовых полей, поэтому план никогда не был хорошим. К сожалению, ISO C ++ не обеспечивает гарантированного арифметического сдвига вправо, но на всех фактических компиляторах x86-64, >> на целом числе со знаком является арифметическим сдвигом дополнения до 2. Если вы чтобы быть очень осторожным, избегая UB, сделайте сдвиг влево на тип без знака, чтобы избежать переполнения целого числа со знаком.

int64_t гарантированно будет типом дополнения 2 без дополнения, если оно существует.

Я думаю, что int64_t на самом деле лучший выбор, чем intptr_t, потому что, если у вас есть 32-битные указатели, например, Linux x32 ABI (32-разрядные указатели в длинном режиме x86-64) , ваш код может все еще просто работать, а приведение uint64_t к типу указателя просто отбрасывает старшие биты. Так что не имеет значения, что вы с ними сделали, и сначала мы надеемся, что нулевое расширение будет оптимизировано.

Таким образом, ваш член uint64_t в конечном итоге будет хранить указатель в младшем 32 и биты вашего тега в старшем 32, что несколько неэффективно, но все еще работает. Может быть, отметьте sizeof(void*) в шаблоне, чтобы выбрать реализацию?


Будущие доказательства

x86-64 ЦП с 5-уровневыми таблицами страниц для 57-разрядные канонические адреса, вероятно, скоро появятся , чтобы позволить использовать энергонезависимое хранилище с большой памятью, такое как Optane / 3DXPoint NVDIMM.

Корпорация Intel уже опубликовала предложение по расширению PML5 https://software.intel.com/sites/default/files/managed/2b/80/5-level_paging_white_paper.pdf (краткую информацию см. В https://en.wikipedia.org/wiki/Intel_5-level_paging). В ядре Linux уже есть поддержка, поэтому он готов к появлению реального HW.

(Я не могу узнать, ожидается ли это в Ледяном озере или нет.)

См. Также Почему в 64-битном виртуальном адресе длина 4 бита (48 бит) по сравнению с физическим адресом (длиной 52 бита)? для получения дополнительной информации о происхождении ограничения 48-битного виртуального адреса.


Таким образом, вы все равно можете использовать старшие 7 бит для теговых указателей и поддерживать совместимость с PML5.

Если вы предполагаете пользовательское пространство, то вы можете использовать верхние 8 бит и нулевое расширение, потому что вы предполагаете, что 57-й бит (бит 56) = 0.

Повторение расширения знака (или нуля) младших битов уже было оптимальным, мы просто меняем его на другую ширину, которая только расширяет биты, которые мы нарушаем . И мы потревожили несколько достаточно высоких битов, чтобы это могло быть в будущем даже в системах, которые поддерживают режим PML5 и используют широкие виртуальные адреса.

В системе с 48-битными виртуальными адресами широковещательный бит 57 на старшие 7 все еще работает, потому что бит 57 = бит 48. И если вы не потревожите эти младшие биты, их не нужно повторно


И, кстати, ваш GetUID() возвращает целое число.Непонятно, зачем вам возвращать статический адрес.

И кстати, для него может быть дешевле вернуть &uid (просто REA-относительный LEA), чем загружать + повторно канонизировать ваш m членское значение.Переместите static const uint64_t uid = 0xBEA57; в статическую переменную-член вместо того, чтобы находиться внутри одной функции-члена.

...