CensoredUsername / dynasm-rs

A dynasm-like tool for rust.
https://censoredusername.github.io/dynasm-rs/language/index.html
Mozilla Public License 2.0
705 stars 52 forks source link

Consider using `MAP_JIT` on macOS #73

Open mkeeter opened 1 year ago

mkeeter commented 1 year ago

On macOS, the recommended way to implement a JIT system is by creating the memory map with PROT_WRITE | PROT_EXEC and the MAP_JIT flag, then using pthread_jit_write_protect_np to switch between writing and executing the buffer.

(This is kinda weird, because the W^X behavior is tracked on a per-thread basis, rather than per-region; I found it easiest to only enable W right before copying into the region, then disable it afterwards)

Anyways, it turns out that this is much faster than using mmap to swap regions from PROT_WRITE to PROT_EXEC!

Here's a flamegraph using mmap

Screen Shot 2022-10-12 at 8 39 43 AM

(note the calls to mprotect and memmove taking up a good chunk of time)

Here's what it looks like with pthread_jit_write_protect_np

Screen Shot 2022-10-12 at 8 40 18 AM

(those calls are gone, and pthread_jit_write_protect_np doesn't even show up)

I see one benchmark go from 112 ms down to 62 ms, almost a 50% improvement!

(My benchmarks are admittedly weird, in that they compile a lot of very small functions šŸ˜†)

This requires ditching / forking the memmap2 crate, which doesn't support this behavior. Here's how I did it.

Right now, it's easy for users to do this on their own: I'm using a VecAssembler then copying into this custom struct Mmap, which works fine. Still, this would be a decent optimization for the stock Assembler.

As always, the dynasm-rs is great, and I really appreciate the work that went into it!

CensoredUsername commented 1 year ago

Interesting, seems like Apple probably also ran into perf bottlenecks with memory protection swapping due to needing to JIT x64 code, so they added a thread-based switch that alters the behaviour of page table permission checks without needing to edit them (and the fairly expensive cache flushes that that tends to cause).

I have no apple hardware myself (I'm just a student who wrote this initially for a fun side project). So it's hard to validate these things, but then again, this is the reason dynasmrt exports basically all the needed components to construct your own custom assemblers as there's too many different but valid ways that you might want to manage your JIT memory. It's therefore probably not needed for this support to be in tree, but let me know if you're missing any components that could be reused outside of the internals of dynasmrt that aren't exported.