Skip to content

Improve performance - #205

Merged
tenderlove merged 10 commits into
ruby:masterfrom
jhawthorn:perf
Jul 23, 2026
Merged

Improve performance#205
tenderlove merged 10 commits into
ruby:masterfrom
jhawthorn:perf

Conversation

@jhawthorn

Copy link
Copy Markdown
Member

No description provided.

@jhawthorn
jhawthorn marked this pull request as ready for review July 13, 2026 17:34
@tenderlove
tenderlove merged commit 5f09107 into ruby:master Jul 23, 2026
67 checks passed
jeremy added a commit to jeremy/fiddle that referenced this pull request Aug 12, 2026
A TYPE_CONST_STRING argument that comes from a to_str object is coerced
inside Fiddle, so the caller's argv holds that object and not the String
that the char * points into. Only converted_args holds the coerced
String, and the conservative scan of the ALLOCV buffer is what keeps it
in place while ffi_call runs without the GVL.

Nothing tested that. Before ruby#205, converted_args was a Ruby Array, whose
elements the GC may move, and the C function read the slot the String
left. ruby#205 replaced the Array with this buffer and corrected the fault as
a side effect. A later change could move these values back to storage the
GC may relocate, and no test would notice.

Add the missing test, and record the requirement next to the buffer.
kou pushed a commit that referenced this pull request Aug 13, 2026
## Problem

`TYPE_CONST_STRING` stores a pointer into a Ruby String:

```c
case TYPE_CONST_STRING:
    ...
    else { dst->pointer = rb_string_value_cstr(src); }
```

A String argument is safe. `argv` holds it and keeps it in place.

A `to_str` object is not. `rb_string_value_cstr` writes the coerced
String back into `src`. `argv` still holds the original object, not the
String. The next loop turn overwrites `src`. After that, only
`converted_args` holds the String.

The pointer must stay valid until `ffi_call` returns. `ffi_call` runs
under `rb_thread_call_without_gvl`, so another thread can compact the
heap during the call. `converted_args` must keep the String **in
place**, not only alive.

Master does this. `converted_args` sits in the `ALLOCV` buffer. The GC
scans that buffer conservatively, and a conservative scan pins the
object.

## Gap

No test covers this.

Before #205, `converted_args` was a Ruby Array. The GC may move an
Array's elements, so the String moved while the pointer stayed, and the
C function read the vacated slot. #205 replaced the Array with the
buffer and fixed the fault as a side effect. Nothing records that the
buffer is required.

## Change

1. `test_call_const_string_from_to_str_after_compaction` in
`test/fiddle/test_function.rb`.
2. A comment at `converted_args` in `ext/fiddle/function.c` stating the
requirement.

The test calls `strcspn` as `[TYPE_CONST_STRING, TYPE_VOIDP] ->
TYPE_SIZE_T`. Arg 0 is a `to_str` object returning a fresh 100-byte
String. Arg 1 is a `to_ptr` object, which makes Fiddle call
`Pointer.[]`; that allocation lets the GC run after the pointer is
stored.

The subject holds no `"Z"`; the reject set is `"Z"`. A correct call
returns 100. A stale pointer returns 0, because a vacated slot holds
zero bytes.

Three details are load-bearing. Remove any one and the test passes on a
faulty build; each has a comment:

- Churn is the subject's byte size, half of it dropped. The compactor
only evacuates non-full pages.
- `to_ptr` returns a **new** `Pointer` each time. An existing `Pointer`
gives Fiddle nothing to allocate.
- The intact-case call runs **after** the loop. A call before it
prepares the CIF, and that preparation is part of what opens the window.

## Result

| tree | arm64-darwin, ruby 3.4.10 | aarch64-linux, ruby 3.4.7 |
|---|---|---|
| `d389bbf` (before #205) | fail | fail |
| master | pass | pass |

Three runs per tree on darwin, same result each time. Full suite on
master with this change: 242 tests, 668 assertions, 0 failures, 1 error,
3 omissions. The error is `test_nogvl_poll` needing `envutil`; it fails
the same way without this change.

Environment: ruby 3.4.10 [arm64-darwin23]; ruby 3.4.7 [aarch64-linux],
glibc 2.36, libffi 3.4.4.

## Note

Released versions still have the fault. I measured 1.1.6 and 1.1.8 on
both platforms: a two-argument call of this shape returns the wrong
result on every iteration under `GC.stress` with `GC.auto_compact`.
Master is correct. You may want a release.

I report this as latent. The trigger is the declared argument type plus
a `to_str` object in place of a String. In one application that loads
fiddle at boot, all 227 bundled gems put every `Fiddle::Function.new`
behind a Windows-only branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants