Почему локальное хранилище потока не реализовано с отображениями таблицы страниц?
Я надеялся использовать C++11 thread_local
ключевое слово для логического флага для потока, к которому будет обращаться очень часто.
Однако большинство компиляторов, по-видимому, реализовали локальное хранилище потока с таблицей, которая отображает целочисленные идентификаторы (слоты) на адрес переменной в текущем потоке. Этот поиск будет происходить внутри пути к критичному по производительности коду, поэтому у меня есть некоторые опасения по поводу его производительности.
Я ожидал, что локальное хранилище потока будет реализовано путем выделения диапазонов виртуальной памяти, которые поддерживаются разными физическими страницами в зависимости от потока. Таким образом, доступ к флагу будет стоить столько же, сколько любой другой доступ к памяти, поскольку MMU заботится о отображении.
Почему ни один из основных компиляторов не использует преимущества отображения таблиц страниц таким образом?
Я полагаю, что я могу реализовать свою собственную "специфичную для потока страницу" с mmap
в Linux и VirtualAlloc
на Win32, но это похоже на довольно распространенный вариант использования. Если кто-то знает о существующих или лучших решениях, пожалуйста, укажите мне на них.
Я также подумал о хранении std::atomic<std::thread::id>
внутри каждого объекта для представления активного потока, но профилирование показывает, что проверка для std::this_thread::get_id() == active_thread
это довольно дорого
6 ответов
В потоке Linux/x86-64 локальное хранилище реализовано через специальный сегментный регистр %fs
(согласно x86-64 ABI стр. 23...)
Итак, следующий код (я использую расширение C + GCC __thread
синтаксис, но он такой же, как C++11 thread_local
)
__thread int x;
int f(void) { return x; }
компилируется (с gcc -O -fverbose-asm -S
) в:
.text
.Ltext0:
.globl f
.type f, @function
f:
.LFB0:
.file 1 "tl.c"
.loc 1 3 0
.cfi_startproc
.loc 1 3 0
movl %fs:x@tpoff, %eax # x,
ret
.cfi_endproc
.LFE0:
.size f, .-f
.globl x
.section .tbss,"awT",@nobits
.align 4
.type x, @object
.size x, 4
x:
.zero 4
Поэтому, вопреки вашим опасениям, доступ к TLS на Linux/x86-64 действительно быстрый. Он не совсем реализован в виде таблицы (вместо этого ядро и среда выполнения управляют %fs
регистр сегмента указывает на область памяти, специфичную для потока, а компилятор и компоновщик управляют смещением там). Тем не менее, старый pthread_getspecific действительно прошел через таблицу, но почти бесполезен, если у вас есть TLS.
Кстати, по определению, все потоки в одном и том же процессе совместно используют одно и то же адресное пространство в виртуальной памяти, поскольку процесс имеет свое собственное единое адресное пространство. (увидеть /proc/self/maps
и т.д... см. proc(5) для более подробной информации /proc/
, а также mmap (2); библиотека потоков C++11 основана на pthreads, которые реализованы с использованием clone (2)). Таким образом, "отображение памяти для конкретного потока" является противоречием: как только задача (то, что запускается планировщиком ядра) имеет свое собственное адресное пространство, она называется процессом (а не потоком). Определяющей характеристикой потоков в одном и том же процессе является совместное использование общего адресного пространства (и некоторых других объектов, таких как файловые дескрипторы).
Предложение не работает, потому что это предотвратит доступ других потоков к вашему thread_local
переменные через указатель. Эти потоки в конечном итоге получат доступ к своей собственной копии этой переменной.
Скажем, например, что у вас есть основной поток и 100 рабочих потоков. Работник_потоки передают указатель на свой собственный thread_local
Переменная обратно в основной поток. Основной поток теперь имеет 100 указателей на эти 100 переменных. Если бы память TLS отображалась в таблице страниц в соответствии с предложением, основной поток имел бы 100 идентичных указателей на одну неинициализированную переменную в TLS основного потока - конечно, не то, что предполагалось!
Операционные системы основного потока, такие как Linux, OSX, Windows, отображают страницы для каждого процесса, а не для каждого потока. Для этого есть очень веская причина: таблицы отображения страниц хранятся в ОЗУ, и их чтение для вычисления эффективного физического адреса будет чрезмерно дорогостоящим, если это необходимо сделать для каждой инструкции.
Так что процессор не делает, он сохраняет копию недавно использованных записей таблицы отображения в быстрой памяти, которая близка к ядру выполнения. Вызывается кеш TLB.
Аннулирование кэша TLB очень дорого, его необходимо перезагрузить из ОЗУ с малой вероятностью того, что данные будут доступны в одном из кэшей памяти. Процессор может остановиться на тысячи циклов, когда это должно произойти.
Таким образом, предложенная вами схема на самом деле может оказаться очень неэффективной, если предположить, что операционная система ее поддержит, а поиск по индексу дешевле. Процессоры очень хороши в простой математике, происходят в гигагерцах, доступ к памяти происходит в мегагерцах.
Отображения памяти не для каждого потока, а для процесса. Все потоки будут иметь одинаковое отображение.
Ядро может предлагать сопоставления для каждого потока, но в настоящее время это не так.
Одной из современных проблем являются аппаратные ограничения (хотя, я уверен, это предшествует ситуациям ниже).
На процессорах SPARC T5 каждый аппаратный поток имеет свой собственный MMU, но совместно использует TLB с семью родственными потоками на одном и том же ядре, и этот TLB может быть подвергнут жесткой обработке.
В MIPS различные отображения памяти для потоков могут заставить их сериализоваться в один контекст выполнения виртуального потока. Это потому, что контексты аппаратного потока совместно используют MMU. Ядро уже не может запускать несколько процессов в контекстах соседних потоков, и отдельные отображения памяти для каждого потока будут иметь такое же ограничение.
Вы используете C++. Имейте объект потока на поток, с рабочей процедурой потока и всеми / большинством вызванных функций, являющихся функциями-членами этого объекта. Тогда вы можете иметь идентификатор потока или любые другие специфичные для потока данные в качестве переменных-членов.