mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
Re-protect only the JIT code made writable on Windows
The JITs pass the whole mapped region to rb_jit_mark_executable() after every write session, and VirtualProtect(), unlike mprotect(), takes time for every page in it even when the protection does not change. Record the ranges rb_jit_mark_writable() commits and protect only those again. YJIT compile_time in ruby-bench, median of 5 runs pinned to one core: 30k_variables 53.3s -> 2.9s, 30k_ifelse 5.4s -> 1.1s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0511c698a0
commit
198b42b706
1 file changed
+74
-7
Notes:
git
2026-10-14 00:30:06 +00:00
@@ -860,12 +860,65 @@ rb_jit_for_each_iseq(rb_iseq_callback callback, void *data)
|
||||
rb_objspace_each_objects(for_each_iseq_i, (void *)&callback_data);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// The JITs pass the whole mapped region to rb_jit_mark_executable() at the
|
||||
// end of every write session, and VirtualProtect() takes time for every page
|
||||
// in the range, unlike mprotect() which skips mappings already protected so.
|
||||
// Remember the ranges made writable, which all lie in that region, and protect
|
||||
// only those again. The JITs call these only at boot or under the VM lock, so
|
||||
// the record needs no lock of its own.
|
||||
#define JIT_WRITABLE_RANGES 64
|
||||
static struct { uintptr_t start, end; } jit_writable[JIT_WRITABLE_RANGES];
|
||||
static int jit_writable_count;
|
||||
static bool jit_writable_overflow;
|
||||
|
||||
static void
|
||||
jit_writable_add(uintptr_t start, uintptr_t end)
|
||||
{
|
||||
if (jit_writable_overflow) return;
|
||||
// Absorb every range the new one overlaps or touches. The ranges stay
|
||||
// apart from each other, so one pass is enough.
|
||||
int kept = 0;
|
||||
for (int i = 0; i < jit_writable_count; i++) {
|
||||
if (start <= jit_writable[i].end && jit_writable[i].start <= end) {
|
||||
if (jit_writable[i].start < start) start = jit_writable[i].start;
|
||||
if (jit_writable[i].end > end) end = jit_writable[i].end;
|
||||
}
|
||||
else {
|
||||
jit_writable[kept++] = jit_writable[i];
|
||||
}
|
||||
}
|
||||
jit_writable_count = kept;
|
||||
if (jit_writable_count == JIT_WRITABLE_RANGES) {
|
||||
jit_writable_overflow = true;
|
||||
return;
|
||||
}
|
||||
jit_writable[jit_writable_count].start = start;
|
||||
jit_writable[jit_writable_count].end = end;
|
||||
jit_writable_count++;
|
||||
}
|
||||
|
||||
static void
|
||||
jit_protect_executable(void *mem_block, size_t mem_size)
|
||||
{
|
||||
DWORD old_protect;
|
||||
if (!VirtualProtect(mem_block, mem_size, PAGE_EXECUTE_READ, &old_protect)) {
|
||||
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, error: %lu",
|
||||
mem_block, (unsigned long)mem_size, GetLastError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool
|
||||
rb_jit_mark_writable(void *mem_block, uint32_t mem_size)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// MEM_COMMIT also re-protects pages that are already committed.
|
||||
return VirtualAlloc(mem_block, mem_size, MEM_COMMIT, PAGE_READWRITE) != NULL;
|
||||
if (VirtualAlloc(mem_block, mem_size, MEM_COMMIT, PAGE_READWRITE) == NULL) {
|
||||
return false;
|
||||
}
|
||||
jit_writable_add((uintptr_t)mem_block, (uintptr_t)mem_block + mem_size);
|
||||
return true;
|
||||
#else
|
||||
return mprotect(mem_block, mem_size, PROT_READ | PROT_WRITE) == 0;
|
||||
#endif
|
||||
@@ -880,11 +933,24 @@ rb_jit_mark_executable(void *mem_block, uint32_t mem_size)
|
||||
return;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
DWORD old_protect;
|
||||
if (!VirtualProtect(mem_block, mem_size, PAGE_EXECUTE_READ, &old_protect)) {
|
||||
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, error: %lu",
|
||||
mem_block, (unsigned long)mem_size, GetLastError());
|
||||
if (jit_writable_overflow) {
|
||||
jit_protect_executable(mem_block, mem_size);
|
||||
jit_writable_count = 0;
|
||||
jit_writable_overflow = false;
|
||||
return;
|
||||
}
|
||||
uintptr_t start = (uintptr_t)mem_block, end = start + mem_size;
|
||||
int kept = 0;
|
||||
for (int i = 0; i < jit_writable_count; i++) {
|
||||
uintptr_t s = jit_writable[i].start, e = jit_writable[i].end;
|
||||
if (start <= s && e <= end) {
|
||||
jit_protect_executable((void *)s, e - s);
|
||||
}
|
||||
else {
|
||||
jit_writable[kept++] = jit_writable[i];
|
||||
}
|
||||
}
|
||||
jit_writable_count = kept;
|
||||
#else
|
||||
if (mprotect(mem_block, mem_size, PROT_READ | PROT_EXEC)) {
|
||||
rb_bug("Couldn't make JIT page (%p, %lu bytes) executable, errno: %s",
|
||||
@@ -898,8 +964,9 @@ bool
|
||||
rb_jit_mark_unused(void *mem_block, uint32_t mem_size)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Keep the pages committed, since rb_jit_mark_executable() covers the whole
|
||||
// mapped region and VirtualProtect() fails on decommitted pages.
|
||||
// Keep the pages committed, since rb_jit_mark_executable() protects the
|
||||
// whole mapped region once its record overflows, and VirtualProtect()
|
||||
// fails on decommitted pages.
|
||||
DWORD old_protect;
|
||||
VirtualAlloc(mem_block, mem_size, MEM_RESET, PAGE_NOACCESS);
|
||||
return VirtualProtect(mem_block, mem_size, PAGE_NOACCESS, &old_protect) != 0;
|
||||
|
||||
Reference in new issue
Block a user