Использование дополнительных 16 бит в 64-битных указателях
Я читал, что 64-битная машина на самом деле использует только 48-битный адрес (в частности, я использую Intel Core i7).
Я ожидаю, что дополнительные 16 бит (биты 48-63) не имеют значения для адреса и будут игнорироваться. Но когда я пытаюсь получить доступ к такому адресу, я получил сигнал EXC_BAD_ACCESS
,
Мой код:
int *p1 = &val;
int *p2 = (int *)((long)p1 | 1ll<<48);//set bit 48, which should be irrelevant
int v = *p2; //Here I receive a signal EXC_BAD_ACCESS.
Почему это так? Есть ли способ использовать эти 16 бит?
Это может быть использовано для создания более дружественного к кешу связанного списка. Вместо использования 8 байтов для следующего ptr и 8 байтов для ключа (из-за ограничения выравнивания) ключ может быть встроен в указатель.
6 ответов
Биты старшего разряда зарезервированы на случай, если адресная шина будет увеличена в будущем, поэтому вы не можете использовать ее просто так
Архитектура AMD64 определяет 64-битный формат виртуальных адресов, из которых 48 битов младшего разряда используются в текущих реализациях (...) Определение архитектуры позволяет повысить этот предел в будущих реализациях до полных 64 бит, расширяя виртуальное адресное пространство до 16 ЭБ (2 64 байта). Это сопоставимо с 4 ГБ (2 32 байта) для x86.
Что еще более важно, согласно той же статье [Акцент на шахте]:
... в первых реализациях архитектуры в преобразовании адресов фактически использовались бы только наименее значимые 48 бит виртуального адреса (поиск в таблице страниц). Кроме того, биты с 48 по 63 любого виртуального адреса должны быть копиями бита 47 (способом, подобным расширению знака), иначе процессор вызовет исключение. Адреса, соответствующие этому правилу, называются "канонической формой".
Поскольку процессор будет проверять старшие биты, даже если они не используются, они на самом деле не являются "неактуальными". Вы должны убедиться, что адрес канонический, прежде чем использовать указатель. Некоторые другие 64-битные архитектуры, такие как ARM64, имеют возможность игнорировать старшие биты, поэтому вы можете хранить данные в указателях гораздо проще.
Тем не менее, в x86_64 вы по-прежнему можете использовать старшие 16 битов, если это необходимо, но вы должны проверить и исправить значение указателя путем расширения знака перед разыменованием его.
Обратите внимание, что приведение значения указателя к long
это не правильный способ сделать, потому что long
не гарантируется, что он будет достаточно широким для хранения указателей. Вам нужно использовать uintptr_t
или же intptr_t
,
int *p1 = &val; // original pointer
uint8_t data = ...;
const uintptr_t MASK = ~(1ULL << 48);
// store data into the pointer
// note: to be on the safe side and future-proof (because future implementations could
// increase the number of significant bits in the pointer), we should store values
// from the most significant bits down to the lower ones
int *p2 = (int *)(((uintptr_t)p1 & MASK) | (data << 56));
// get the data stored in the pointer
data = (uintptr_t)p2 >> 56;
// deference the pointer
// technically implementation defined. You may want a more
// standard-compliant way to sign-extend the value
intptr_t p3 = ((intptr_t)p2 << 16) >> 16; // sign extend the pointer to make it canonical
val = *(int*)p3;
JavaScript WebCit и движок Mozilla SpiderMonkey используют это в технике нанобокса. Если значение равно NaN, младшие 48 битов сохранят указатель на объект, а старшие 16 битов служат битами тега, в противном случае это двойное значение.
Вы также можете использовать младшие биты для хранения данных. Это называется теговым указателем. Если int
выровнен на 4 байта, тогда 2 младших бита всегда равны 0, и вы можете использовать их как в 32-битных архитектурах. Для 64-битных значений вы можете использовать 3 младших бита, поскольку они уже выровнены по 8 байтов. Опять же, вам также нужно очистить эти биты перед разыменованием.
int *p1 = &val; // the pointer we want to store the value into
int tag = 1;
const uintptr_t MASK = ~0x03ULL;
// store the tag
int *p2 = (int *)(((uintptr_t)p1 & MASK) | tag);
// get the tag
tag = (uintptr_t)p2 & 0x03;
// get the referenced data
intptr_t p3 = (uintptr_t)p2 & MASK; // clear the 2 tag bits before using the pointer
val = *(int*)p3;
Один известный пользователь этого - 32-битная версия V8 с оптимизацией SMI (маленькое целое число) (хотя я не уверен насчет 64-битного V8). Младшие биты будут служить тегом для типа: если оно равно 0, это небольшое 31-разрядное целое число, выполните сдвиг вправо со знаком на 1, чтобы восстановить значение; если это 1, значение является указателем на реальные данные (объекты, числа с плавающей запятой или большие целые числа), просто очистите тег и разыменуйте его
Примечание: использование связанного списка для случаев с крошечными значениями ключей по сравнению с указателями является огромной тратой памяти, а также медленнее из-за плохой локализации кэша. На самом деле вы не должны использовать связанный список в большинстве реальных проблем
- Бьерн Страуструп говорит, что мы должны избегать связанных списков
- Почему вы никогда не должны когда-либо снова использовать связанный список в вашем коде
- Сокращение чисел: почему вы никогда, никогда, НИКОГДА не будете снова использовать связанный список в вашем коде
- Бьярне Страуструп: почему вы должны избегать связанных списков
- Списки злые? Бьярн Страуструп
Думаю, никто не упомянул возможное использование битовых полей ( https://en.cppreference.com/w/cpp/language/bit_field) в этом контексте, например
template<typename T>
struct My64Ptr
{
signed long long ptr : 48; // as per phuclv's comment, we need the type to be signed to be sign extended
unsigned long long ch : 8; // ...and, what's more, as Peter Cordes pointed out, it's better to mark signedness of bit field explicitly (before C++14)
unsigned long long b1 : 1; // Additionally, as Peter found out, types can differ by sign and it doesn't mean the beginning of another bit field (MSVC is particularly strict about it: other type == new bit field)
unsigned long long b2 : 1;
unsigned long long b3 : 1;
unsigned long long still5bitsLeft : 5;
inline My64Ptr(T* ptr) : ptr((long long) ptr)
{
}
inline operator T*()
{
return (T*) ptr;
}
inline T* operator->()
{
return (T*)ptr;
}
};
My64Ptr<const char> ptr ("abcdefg");
ptr.ch = 'Z';
ptr.b1 = true;
ptr.still5bitsLeft = 23;
std::cout << ptr << ", char=" << char(ptr.ch) << ", byte1=" << ptr.b1 <<
", 5bitsLeft=" << ptr.still5bitsLeft << " ...BTW: sizeof(ptr)=" << sizeof(ptr);
// The output is: abcdefg, char=Z, byte1=1, 5bitsLeft=23 ...BTW: sizeof(ptr)=8
// With all signed long long fields, the output would be: abcdefg, char=Z, byte1=-1, 5bitsLeft=-9 ...BTW: sizeof(ptr)=8
Я думаю, что это может быть довольно удобный способ попробовать использовать эти 16 бит, если мы действительно хотим сэкономить немного памяти. Все побитовые операции (& и |) и приведение к полному 64-битному указателю выполняются компилятором (хотя, конечно, выполняются во время выполнения).
Соответствующий стандартам способ канонизации указателей AMD/Intel x64 (на основе текущей документации канонических указателей и 48-битной адресации):
int *p2 = (int *)(((uintptr_t)p1 & ((1ull << 48) - 1)) |
~(((uintptr_t)p1 & (1ull << 47)) - 1));
Это сначала очищает старшие 16 бит указателя. Затем, если бит 47 равен 1, это устанавливает биты с 47 по 63, но если бит 47 равен 0, выполняется логическое ИЛИ со значением 0 (без изменений).
Согласно Руководствам Intel (том 1, раздел 3.3.7.1) линейные адреса должны быть в канонической форме. Это означает, что на самом деле используются только 48 бит, а дополнительные 16 бит расширяются. Кроме того, реализация должна проверить, находится ли адрес в этой форме и не генерирует ли он исключение. Вот почему нет возможности использовать эти дополнительные 16 бит.
Причина, по которой это делается таким образом, довольно проста. В настоящее время 48-битное виртуальное адресное пространство более чем достаточно (и из-за стоимости производства ЦП нет смысла увеличивать его), но, несомненно, в будущем понадобятся дополнительные биты. Если приложения / ядра будут использовать их в своих целях, возникнут проблемы с совместимостью, и именно этого производители ЦП хотят избежать.
Физическая память имеет адрес 48 бит. Этого достаточно для обращения к большому объему оперативной памяти. Однако между вашей программой, работающей на ядре ЦП и ОЗУ, находится блок управления памятью, являющийся частью ЦП. Ваша программа обращается к виртуальной памяти, а MMU отвечает за перевод между виртуальными адресами и физическими адресами. Виртуальные адреса являются 64-битными.
Значение виртуального адреса ничего не говорит о соответствующем физическом адресе. Действительно, из-за того, как работают системы виртуальной памяти, нет гарантии, что соответствующий физический адрес будет одинаковым от момента к моменту. И если вы проявите творческий подход с помощью mmap(), вы можете сделать так, чтобы два или более виртуальных адреса указывали на один и тот же физический адрес (где бы это ни было). Если вы затем пишете на любой из этих виртуальных адресов, вы фактически пишете только на один физический адрес (где бы это ни было). Такая хитрость весьма полезна при обработке сигналов.
Таким образом, когда вы вмешиваетесь в 48-й бит вашего указателя (который указывает на виртуальный адрес), MMU не может найти этот новый адрес в таблице памяти, выделенной для вашей программы ОС (или самостоятельно с помощью malloc()), Это вызывает прерывание в знак протеста, ОС ловит это и завершает вашу программу с сигналом, который вы упомянули.
Если вы хотите узнать больше, я предлагаю вам "современную компьютерную архитектуру" Google и немного почитайте об оборудовании, лежащем в основе вашей программы.
Попробуйте распечатать измененный PTR после сдвига битов:
int var{ 1 }; int* p{ &var }; cout << p; p = (int*)((uintptr_t)p | 1ll << 50); cout << " shifted: " << p;
У меня был такой вывод: значение указателя изменилось, но что такое ошибка «Нарушение доступа»?
Эта ошибка означает, что кто-то пытается получить доступ к незарезервированной памяти: /questions/25272731/chto-oznachaet-narushenie-dostupa/25272739#25272739, и когда вы разыменовываете третью строку, вы получаете эту ошибку. Например, у меня была эта ошибка, когда я == 10:
for (int i = 1; i < 64; i++) { p = (int*)((uintptr_t)p | 1ll << i); int v = *p; }