Skip to content

Commit bbd4b0a

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Resolve tstate TLS offset for general-dynamic libpython
Summary: `initThreadStateOffset()` only recognized the initial-exec shape of `_PyThreadState_GetCurrent` (`movq %fs:OFFSET,%rax`). When libpython is built as a shared library the function is compiled with the general-dynamic TLS model instead, `lea &tls_index; call __tls_get_addr; mov disp(%rax),%rax`, so the pattern match failed and `translateLoadThreadState` fell back to emitting a `call _PyThreadState_GetCurrent` and called `__tls_get_addr`) on every thread-state load in JIT-compiled code. Profiling the `inference_pipeline` benchmark showed `__tls_get_addr` accounting for 4-6% of samples in every workload, attributed directly to JIT frames and runtime helpers like `batchDecref`. This adds an x86-64/Linux `decodeGeneralDynamicTstateOffset()` fallback that decodes the general-dynamic sequence, resolves the module TLS block once via `__tls_get_addr`, and computes a thread-pointer-relative offset (the `%fs` base is the thread pointer, and `%fs:0` holds its value). The offset is self-verified against `_PyThreadState_GetCurrent()` before use; on any mismatch it returns `-1` and the existing `call` fallback is kept, so the change is strictly safe. This mirrors the existing aarch64 TLSDESC fallback. With the offset resolved, the fast path emits a single `mov dst, %fs:[offset]`. Reviewed By: mpage Differential Revision: D110197544 fbshipit-source-id: b73c49209667228b25664551842a0ca785879fc8
1 parent ca791c3 commit bbd4b0a

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

cinderx/Jit/codegen/tls.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,62 @@
1212

1313
namespace cinderx::jit::codegen {
1414

15+
namespace {
16+
17+
#if defined(CINDER_X86_64) && defined(__linux__)
18+
// __tls_get_addr is provided by the dynamic loader and has no public header.
19+
extern "C" void* __tls_get_addr(void* tls_index);
20+
21+
// When libpython is a shared library, `_PyThreadState_GetCurrent` is compiled
22+
// with the general-dynamic TLS model and looks like:
23+
// push %rbp; mov %rsp,%rbp
24+
// lea disp32(%rip), %rdi # &tls_index
25+
// call __tls_get_addr
26+
// mov disp32(%rax), %rax # tstate = *(tls_block + disp)
27+
// __tls_get_addr returns the per-thread address of libpython's TLS block. For a
28+
// module loaded at startup that block sits at a fixed offset from the thread
29+
// pointer, so we resolve a thread-pointer-relative offset once here and let the
30+
// JIT emit direct %fs loads instead of calling __tls_get_addr on every tstate
31+
// access. Returns the offset, or -1 if the function doesn't match the expected
32+
// shape or the resolved offset fails verification.
33+
int32_t decodeGeneralDynamicTstateOffset(const uint8_t* ts_func) {
34+
const bool matches = ts_func[0] == 0x55 && // push %rbp
35+
ts_func[1] == 0x48 && ts_func[2] == 0x89 &&
36+
ts_func[3] == 0xe5 && // mov %rsp,%rbp
37+
ts_func[4] == 0x48 && ts_func[5] == 0x8d &&
38+
ts_func[6] == 0x3d && // lea disp32(%rip),%rdi (bytes 4-10)
39+
ts_func[11] == 0xe8 && // call rel32 (bytes 11-15)
40+
ts_func[16] == 0x48 && ts_func[17] == 0x8b &&
41+
ts_func[18] == 0x80; // mov disp32(%rax),%rax (bytes 16-22)
42+
if (!matches) {
43+
return -1;
44+
}
45+
46+
// The lea is RIP-relative to the following instruction, the call at
47+
// ts_func + 11.
48+
const int32_t lea_disp = *reinterpret_cast<const int32_t*>(ts_func + 7);
49+
void* tls_index = const_cast<uint8_t*>(ts_func) + 11 + lea_disp;
50+
const int32_t member_disp = *reinterpret_cast<const int32_t*>(ts_func + 19);
51+
52+
const uintptr_t tls_block =
53+
reinterpret_cast<uintptr_t>(__tls_get_addr(tls_index));
54+
55+
// On x86-64 the %fs base is the thread pointer, and %fs:0 holds its value.
56+
uintptr_t thread_ptr;
57+
asm volatile("mov %%fs:0, %0" : "=r"(thread_ptr));
58+
59+
const int32_t offset =
60+
static_cast<int32_t>(tls_block - thread_ptr) + member_disp;
61+
62+
// Verify the computed offset reads back the real tstate before trusting it.
63+
PyThreadState* from_offset =
64+
*reinterpret_cast<PyThreadState**>(thread_ptr + offset);
65+
return from_offset == _PyThreadState_GetCurrent() ? offset : -1;
66+
}
67+
#endif
68+
69+
} // namespace
70+
1571
void initThreadStateOffset() {
1672
auto module_state = cinderx::getModuleState();
1773
if (module_state->tstate_offset_inited) {
@@ -32,6 +88,14 @@ void initThreadStateOffset() {
3288
ts_func[7] == 0x04 && ts_func[8] == 0x25) { // movq %fs:OFFSET, %rax
3389
module_state->tstate_offset = *reinterpret_cast<int32_t*>(ts_func + 9);
3490
}
91+
#ifdef __linux__
92+
// General-dynamic TLS fallback (the form used when libpython is a shared
93+
// library), so JIT code can load tstate with a direct %fs access rather than
94+
// calling _PyThreadState_GetCurrent / __tls_get_addr on every use.
95+
if (module_state->tstate_offset == -1) {
96+
module_state->tstate_offset = decodeGeneralDynamicTstateOffset(ts_func);
97+
}
98+
#endif
3599
#elif defined(CINDER_AARCH64)
36100
uint32_t* ts_func = reinterpret_cast<uint32_t*>(&_PyThreadState_GetCurrent);
37101

0 commit comments

Comments
 (0)