Three message sends per draw, and what they cost under Rosetta
Calling three Metal methods per draw through cached function pointers instead of Objective-C message sends, plus skipping bindings a repeated draw already has, cut the CPU time of a 64,000-draw scene by a fifth.
What changed
mtl12's unix side, the part that turns recorded D3D12 command lists into Metal 4 calls, runs as x86_64 code under Rosetta. bgfx's drawstress scene issues about 64,000 draws a frame, nearly all with the same state block (the bundle of pipeline, heaps and vertex buffers a draw uses).
Two changes this session:
- The render argument table (Metal 4's list of buffer addresses a shader reads) is no longer re-checked for a draw of the block it was last bound for. Such a draw only sets its root arguments, which are copied and compared with plain word loops instead of calls to libc's memcmp and memcpy.
- The three Objective-C methods every ordinary draw calls (set the root address, bind the table, draw) are looked up once per encoder and then called through their function pointers, the IMPs, instead of through
objc_msgSend.
The interesting part
A calibration loop injected into a Wine process measured 4.4 ns per message send and 1.6 ns per IMP call under Rosetta. For three sends and 64,000 draws that predicts about 0.5 ms. The A/B run saved about 2 ms, or roughly 10 ns per send. Why sends to Metal's objects cost that much more than the warm loop is not measured yet.
The test for the first change took two tries. Dropping its safety condition passed all 75 API tests. Even a new test that read a constant buffer through a descriptor table passed. Constant buffers are read at table slot 5, and the operation that overwrites a slot behind the cache's back, a rectangle clear, writes slot 0. Reading a typed buffer, which goes through slot 0, made the broken build fail 12 checks.
Numbers
- drawstress unix CPU, medians of 3 alternating rounds: 16.86 -> 15.07 -> 13.19 ms; frame 28.70 -> 26.86 -> 25.07 ms; bench-draws submit 2.66 -> 2.44 -> 2.09 ms
- Full suite: 839 pass, 28 fail, 76 skip of 943, the same as entry 059.
- Unexplained: drawstress GPU time went up 6-16% in the runs with the new code.
- before
- + bindings_of
- + cached IMPs
measurement · ms
- drawstress unix CPU16.86 before, 15.07 + bindings_of, 13.19 + cached IMPs
- drawstress frame28.7 before, 26.86 + bindings_of, 25.07 + cached IMPs
- bench-draws submit2.66 before, 2.44 + bindings_of, 2.09 + cached IMPs
- pass
- fail
- skip
suite · share of each row's total
- vkd3d-proton490 pass, 15 fail, 72 skip of 577
- vkd3d177 pass, 9 fail, 4 skip of 190
- apps77 pass, 0 fail, 0 skip of 77
- api75 pass, 0 fail, 0 skip of 75
- wine-dxgi16 pass, 4 fail, 0 skip of 20
- samples4 pass, 0 fail, 0 skip of 4