Я посмотрел на вывод ассемблера для следующего фрагмента кода, и я был ошеломлен:
int x=0, y=0; // global
// r1, r2 are ints, local.
std::thread t([&x, &y, &r1, &r2](){
x = 1;
r1 = y;
});
!std::thread t([&x, &y, &r1, &r2](){
<lambda()>::operator()(void) const+0: push %rbp
<lambda()>::operator()(void) const+1: mov %rsp,%rbp
<lambda()>::operator()(void) const+4: mov %rdi,-0x8(%rbp)
<lambda()>::operator()(void) const+18: mov -0x8(%rbp),%rax
<lambda()>::operator()(void) const+22: mov (%rax),%rax
! x = 1;
<lambda()>::operator()(void) const()
<lambda()>::operator()(void) const+8: movl $0x1,0x205362(%rip) # 0x6062ac <x>
! r1 = y;
<lambda()>::operator()(void) const+25: mov 0x205359(%rip),%edx # 0x6062b0 <y>
<lambda()>::operator()(void) const+31: mov %edx,(%rax)
!
!});
<lambda()>::operator()(void) const+33: nop
<lambda()>::operator()(void) const+34: pop %rbp
<lambda()>::operator()(void) const+35: retq
Почему адрес x
,y
определяется относиться к RIP
, RIP
это указатель на инструкцию, поэтому он кажется диким. Тем более я никогда не видел ничего подобного. (Возможно, я не видел много вещей :)).
Единственное объяснение, которое приходит мне в голову, заключается в том, что лямбда-замыкание и взятие переменных среды из определенного места имеет что-то общее с RIP
,
Код не перемещается во время выполнения, после загрузки секции кода подпрограмма не копируется и не перемещается.
Статические данные также занимают тот же адрес после загрузки их раздела.
Таким образом, расстояние между инструкцией и статической переменной известно во время компиляции, и оно является инвариантным при перемещении базы модуля (поскольку и инструкция, и данные переводятся на одну и ту же величину).
Таким образом, RIP-относительная адресация не только не является дикой, но она всегда отсутствовала.
В то время как в 32-битном коде инструкция вроде mov eax, [var]
безвреден, в 64-битной без RIP-относительной адресации требуется 9 байтов, 1 для кода операции и 8 для немедленной.
С RIP-относительной адресацией непосредственные значения все еще 32-битные.
C ++ lamdbas — это синтаксический сахар для функционального объекта, где захваченные переменные становятся переменными экземпляра.
Переменные, захваченные по ссылке, обрабатываются как указатель / ссылка.
Глобальные переменные не требуют особой обработки при захвате, так как они уже доступны.
Вы справедливо отметили, что x
а также y
Доступ соответственно как 0x205362(%rip)
а также 0x205359(%rip)
,
Поскольку они являются глобальными, их адрес фиксируется во время выполнения, и для доступа к ним используется относительная RIP-адресация.
Однако вы забыли проверить, как r1
, локальная захваченная переменная, доступ.
Хранится с (%rax)
а также rax
был ранее загружен как (оптимизация) movq (%rdi), %rax
,
%rdi
это первый параметр метода operator()
, так что, это this
только что упомянутая инструкция загружает первую переменную экземпляра в rax
а затем использовать это значение для доступа r1
,
Проще говоря, это указатель (или лучше ссылка) на r1
, поскольку r1
живет в стеке, его адрес является динамическим во время выполнения (это зависит от состояния стека).
Таким образом, лямбда использует как косвенную, так и относительную к RIP адресацию, что противоречит гипотезе о том, что относительная к RIP адресация была какой-то особенной.
Обратите внимание, что механизм захвата не продлевает срок службы переменных захвата (как в ECMAScript), поэтому захват локальной переменной по ссылке в лямбда-выражении для std::thread
почти всегда плохая идея.
Других решений пока нет …