Skip to content

core: use the platform's memchr#159090

Draft
joboet wants to merge 1 commit into
rust-lang:mainfrom
joboet:memchr-compiler-builtins
Draft

core: use the platform's memchr#159090
joboet wants to merge 1 commit into
rust-lang:mainfrom
joboet:memchr-compiler-builtins

Conversation

@joboet

@joboet joboet commented Jul 10, 2026

Copy link
Copy Markdown
Member

The custom, SWAR-based memchr implementation is in many cases slower than the platform's own implementation. This is especially the case on GNU/Linux as glibc will use IFUNCs to select the most efficient implementation at link time. Thus, this PR makes core unconditionally use the platform memchr.

Although one could also imagine including the memchr crate into core, platforms are in my opinion in a much better place to optimise their implementation – even some don't – since they can e.g. update their dynamically-linked libc to take advantage of new target features and know better than us whether to optimise for binary-size or performance.

Just like #94079 did, this PR adds a new symbol to the ones required by core. Given that memchr is in the C standard just like strlen is, I expect every platform that already provides the other symbols will also provide memchr. This PR also adds a reasonably optimised memchr implementation to compiler_builtins so that all targets that use the memory routines contained therein will continue to work.

r? @tgross35

@rustbot

rustbot commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

compiler-builtins is developed in its own repository. If possible, consider making this change to rust-lang/compiler-builtins instead.

cc @tgross35

⚠️ #[rustc_allow_const_fn_unstable] needs careful audit to avoid accidentally exposing unstable
implementation details on stable.

cc @rust-lang/wg-const-eval

@rustbot rustbot added A-compiler-builtins Area: compiler-builtins (https://github.com/rust-lang/compiler-builtins) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 10, 2026
@rustbot

rustbot commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

tgross35 is currently at their maximum review capacity.
They may take a while to respond.

// describe a valid memory region. Since the reference is a `&[u8]`,
// every byte contained therein is interpretable as an initialized
// byte.
let res = unsafe { memchr(text.as_ptr().cast(), x as c_int, text.len()) };

@RalfJung RalfJung Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the slice is empty, it might legally be a no-provenance pointer. C requires the memchr pointer to always be valid, even if the length is 0. In that case, this call would cause UB (that Miri would flag).

For some of the other operations, we assume that they work on arbitrary pointers if the length is 0, and there are recent proposals to the C standard to officially bless this. Do those proposals cover memchr as well?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have though the validity requirement was only for null pointers, which cannot occur here since the pointer comes from a slice. In any case, N3322 covers memchr.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have though the validity requirement was only for null pointers, which cannot occur here since the pointer comes from a slice.

In C it is UB to even create a pointer that doesn't point to an allocation (except for null), so I see no way to argue that it would be allowed to pass such a pointer when even the much-more-well-defined null pointer is disallowed.

In any case, N3322 covers memchr.

Okay, good.
Still, please add this to the libcore crate-level docs as an assumption we make, since it's not yet in a published standard.

And we have to figure out what to do with Miri. Currently, if you directly invoke memcpy or any of the others in Miri, it still enforces the C23 rules. Only if you directly invoke Rust's intrinsics do we allow arbitrary dangling pointers for size 0. For memchr we thus have to either also introduce an intrinsic, or we have to make Miri implement the rules of https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3322.pdf in the hopes that the committee will include them in the next standard. Do you have any idea how far along that proposal is through the process? (Cc @nikic)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In C it is UB to even create a pointer that doesn't point to an allocation (except for null), so I see no way to argue that it would be allowed to pass such a pointer when even the much-more-well-defined null pointer is disallowed.

But one-past-the-end pointers are allowed to be created, and so I'd expect the following to work.

char buf[16];
memchr(&buf + 16, 42, 0);

@RalfJung RalfJung Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's entirely fine.

But this isn't:

memchr((char*)16, 42, 0);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it means that memchr cannot make any reads if n is zero – it cannot assume anything about the memory before the pointer, and it must not read from the pointer. So I'd say that your example is sound, too (modulo the int2ptr).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The int2ptr is UB. And because such an int2ptr is UB I don't think it is valid to pass such a ptr to C even if you can create it in Rust UB-free. It's basically a validity invariant for pointers in C that they must be associated with a live allocation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that this is all too relevant here. I hope you agree that the following is the maximally-strict story code for memchr:

fn memchr(mut s: *const u8, v: i32, n: usize) -> *mut u8 {
    assert_unchecked(!s.is_null());
    let needle = v as u8;
    while n > 0 {
        let byte = s.read();
        if byte == needle {
            return s.cast_mut();
        }
        
        s = s.add(1);
        n -= 1;
    }

    ptr::null_mut()
}

But if you hold that s has to be associated with a live allocation, then it isn't. Because Rust's AM does not have a way to test for live allocations, there wouldn't be any story code that matches the behaviour of memchr.

So this poses the fundamental question of whether the Rust AM can be lowered to an abstract machine that has undefined behaviour not expressible in Rust. While this would be a correct lowering, I don't think it should be allowed, as it makes it impossible to reason about FFI in Rust without reasoning about an arbitrary, cross-language AM.

@RalfJung RalfJung Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you call code written in a different language, you have to follow its rules. Calling memchr with (char*)16 is entirely unproblematic on the Rust side (so writing story code in Rust misses the point), but the value you are passing to the C Abstract Machine simply does not make sense there, and hence you have UB.

IOW, you have to make both Abstract Machines happy. And in this case, while the Rust AM is happy, the C AM is not. (Or at least, I haven't yet seen a good argument for why it should be happy.)

@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

The job x86_64-gnu-next-trait-solver-polonius failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
[RUSTC-TIMING] build_script_build test:false 0.255
error[E0425]: cannot find value `ptr` in this scope
  --> library/core/src/slice/memchr.rs:63:63
   |
63 |                 let index = unsafe { res.offset_from_unsigned(ptr) };
   |                                                               ^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.
[RUSTC-TIMING] core test:false 24.808
error: could not compile `core` (lib) due to 1 previous error

Important

For more information how to resolve CI failures of this job, visit this link.

@tgross35 tgross35 Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The c-b part should land as a PR to rust-lang/compiler-builtins first. We're not running all its checks and benchmarks in r-l/r

View changes since the review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I think this would be good to add regardless of whether or not we use it, if only for the purpose of helping no-std builds)

@joboet
joboet marked this pull request as draft July 10, 2026 14:59
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 10, 2026
@tgross35

Copy link
Copy Markdown
Contributor

Nominating for @rust-lang/libs because there are some tradeoffs here. Notably in #t-libs > Using the platform's memchr, the possibility of using the memchr crate as a submodule was brought up instead.

The top post says:

Although one could also imagine including the memchr crate into core, platforms are in my opinion in a much better place to optimise their implementation – even some don't – since they can e.g. update their dynamically-linked libc to take advantage of new target features and know better than us whether to optimise for binary-size or performance.

We could also use the memchr crate now and switch to the platform versions if/when they catch up. Using the crate just seems like an easier way to give everyone the same performance - if there are platforms that have gone the past decade with an underoptimized memchr, I'm not sure there's really any reason to expect that to change.

@rustbot label +I-libs-nominated

@rustbot rustbot added the I-libs-nominated Nominated for discussion during a libs team meeting. label Jul 10, 2026
@BurntSushi

Copy link
Copy Markdown
Member

IMO, the memchr crate is the superior option here. I'm biased of course, but I think this PR is probably understating how little the platform memchr can be optimized. e.g., I think musl is just limited to the SWAR approach. And I don't know what Windows does.

In contrast, the memchr crate should have a great implementation on more platforms. That includes x86-64, aarch64 and wasm32. For other platforms, it falls back to SWAR.

Here are some benchmarks comparing the memchr crate on x86-64:

[andrew@duff memchr]$ rebar cmp ./benchmarks/record/aarch64/2023-12-29.csv -e 'libc/memchr' -e 'rust/memchr/memchr/oneshot' -e 'rust/memchr/memchr/prebuilt'
benchmark                          libc/memchr/oneshot  rust/memchr/memchr/oneshot  rust/memchr/memchr/prebuilt
---------                          -------------------  --------------------------  ---------------------------
memchr/sherlock/common/huge1       1200.7 MB/s (1.33x)  1490.7 MB/s (1.07x)         1594.1 MB/s (1.00x)
memchr/sherlock/common/small1      2.1 GB/s (1.17x)     2.5 GB/s (1.00x)            2.5 GB/s (1.00x)
memchr/sherlock/common/tiny1       1566.8 MB/s (1.02x)  1566.8 MB/s (1.02x)         1605.0 MB/s (1.00x)
memchr/sherlock/never/huge1        44.3 GB/s (1.67x)    73.9 GB/s (1.00x)           73.9 GB/s (1.00x)
memchr/sherlock/never/small1       15.1 GB/s (41.00x)   618.4 GB/s (1.00x)          618.4 GB/s (1.00x)
memchr/sherlock/never/tiny1        64.3 GB/s (1.00x)    64.3 GB/s (1.00x)           64.3 GB/s (1.00x)
memchr/sherlock/never/empty1       1.00ns (1.00x)       1.00ns (1.00x)              1.00ns (1.00x)
memchr/sherlock/rare/huge1         38.0 GB/s (1.63x)    61.6 GB/s (1.00x)           61.8 GB/s (1.00x)
memchr/sherlock/rare/small1        15.1 GB/s (41.00x)   618.4 GB/s (1.00x)          618.4 GB/s (1.00x)
memchr/sherlock/rare/tiny1         64.3 GB/s (1.00x)    64.3 GB/s (1.00x)           64.3 GB/s (1.00x)
memchr/sherlock/uncommon/huge1     5.5 GB/s (1.21x)     4.9 GB/s (1.36x)            6.6 GB/s (1.00x)
memchr/sherlock/uncommon/small1    14.7 GB/s (1.02x)    14.7 GB/s (1.02x)           15.1 GB/s (1.00x)
memchr/sherlock/uncommon/tiny1     64.3 GB/s (1.00x)    64.3 GB/s (1.00x)           64.3 GB/s (1.00x)
memchr/sherlock/verycommon/huge1   535.3 MB/s (1.81x)   881.4 MB/s (1.10x)          970.7 MB/s (1.00x)
memchr/sherlock/verycommon/small1  844.3 MB/s (1.29x)   950.8 MB/s (1.14x)          1086.2 MB/s (1.00x)

And aarch64:

[andrew@duff memchr]$ rebar cmp ./benchmarks/record/x86_64/2025-09-25.csv -e 'libc/memchr' -e 'rust/memchr/memchr/oneshot' -e 'rust/memchr/memchr/prebuilt'
benchmark                          libc/memchr/oneshot  rust/memchr/memchr/oneshot  rust/memchr/memchr/prebuilt
---------                          -------------------  --------------------------  ---------------------------
memchr/sherlock/common/huge1       3.2 GB/s (1.04x)     3.1 GB/s (1.07x)            3.3 GB/s (1.00x)
memchr/sherlock/common/small1      3.4 GB/s (1.10x)     3.4 GB/s (1.10x)            3.7 GB/s (1.00x)
memchr/sherlock/common/tiny1       1241.6 MB/s (1.04x)  1218.6 MB/s (1.06x)         1290.3 MB/s (1.00x)
memchr/sherlock/never/huge1        133.8 GB/s (1.01x)   135.1 GB/s (1.00x)          135.1 GB/s (1.00x)
memchr/sherlock/never/small1       44.2 GB/s (1.00x)    41.2 GB/s (1.07x)           41.2 GB/s (1.07x)
memchr/sherlock/never/tiny1        5.4 GB/s (1.00x)     4.9 GB/s (1.08x)            4.9 GB/s (1.08x)
memchr/sherlock/never/empty1       11.00ns (1.00x)      11.00ns (1.00x)             12.00ns (1.09x)
memchr/sherlock/rare/huge1         109.9 GB/s (1.04x)   112.8 GB/s (1.01x)          114.5 GB/s (1.00x)
memchr/sherlock/rare/small1        32.5 GB/s (1.06x)    32.5 GB/s (1.06x)           34.4 GB/s (1.00x)
memchr/sherlock/rare/tiny1         4.3 GB/s (1.00x)     4.3 GB/s (1.00x)            4.3 GB/s (1.00x)
memchr/sherlock/uncommon/huge1     16.2 GB/s (1.00x)    11.5 GB/s (1.41x)           11.6 GB/s (1.40x)
memchr/sherlock/uncommon/small1    13.4 GB/s (1.05x)    13.4 GB/s (1.05x)           14.1 GB/s (1.00x)
memchr/sherlock/uncommon/tiny1     2.2 GB/s (1.00x)     2.0 GB/s (1.10x)            2.1 GB/s (1.03x)
memchr/sherlock/verycommon/huge1   1436.4 MB/s (1.11x)  1451.5 MB/s (1.10x)         1599.3 MB/s (1.00x)
memchr/sherlock/verycommon/small1  1469.2 MB/s (1.09x)  1455.7 MB/s (1.10x)         1607.2 MB/s (1.00x)

@Kobzol

Kobzol commented Jul 11, 2026

Copy link
Copy Markdown
Member

@bors try @rust-timer queue

@rust-timer

Copy link
Copy Markdown
Collaborator

Awaiting bors try build completion.

@rustbot label: +S-waiting-on-perf

@rust-bors

This comment has been minimized.

@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Jul 11, 2026
rust-bors Bot pushed a commit that referenced this pull request Jul 11, 2026
@rust-bors

rust-bors Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

💔 Test for bb887d8 failed: CI. Failed job:

@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

The job dist-x86_64-linux-quick failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
note: to see what the problems were, use the option `--future-incompat-report`, or run `cargo report future-incompatibilities --id 1`
##[endgroup]
[2026-07-11T13:26:43.881Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO)` starts
[2026-07-11T13:26:43.881Z INFO  opt_dist::timer] Section `Stage 1 (Rustc PGO) > Build PGO instrumented rustc and LLVM` starts
[2026-07-11T13:26:43.881Z INFO  opt_dist::exec] Executing `RUST_BACKTRACE=full python3 /checkout/x.py build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --set pgo.rustc.generate="/tmp/tmp-multistage/opt-artifacts/rustc-pgo" --set llvm.thin-lto=false --set llvm.link-shared=true [at /checkout/obj]`
##[group]Building bootstrap
    Finished `dev` profile [unoptimized] target(s) in 0.06s
##[endgroup]
[TIMING:start] compile::Assemble { target_compiler: Compiler { stage: 2, host: x86_64-unknown-linux-gnu, forced_compiler: false } }
[TIMING:start] builder::Libdir { compiler: Compiler { stage: 2, host: x86_64-unknown-linux-gnu, forced_compiler: false }, target: x86_64-unknown-linux-gnu }
---
[RUSTC-TIMING] build_script_build test:false 0.273
error[E0425]: cannot find value `ptr` in this scope
  --> /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/library/core/src/slice/memchr.rs:63:63
   |
63 |                 let index = unsafe { res.offset_from_unsigned(ptr) };
   |                                                               ^^^ not found in this scope

[RUSTC-TIMING] profiler_builtins test:false 0.022
For more information about this error, try `rustc --explain E0425`.
[RUSTC-TIMING] core test:false 14.265
error: could not compile `core` (lib) due to 1 previous error
Bootstrap failed while executing `build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --set pgo.rustc.generate="/tmp/tmp-multistage/opt-artifacts/rustc-pgo" --set llvm.thin-lto=false --set llvm.link-shared=true`
Currently active steps:
compile::Assemble { target_compiler: Compiler { stage: 2, host: x86_64-unknown-linux-gnu, forced_compiler: false } } at src/bootstrap/src/core/build_steps/compile.rs:142
compile::Rustc { target: x86_64-unknown-linux-gnu, build_compiler: Compiler { stage: 1, host: x86_64-unknown-linux-gnu, forced_compiler: false }, crates: [] } at src/bootstrap/src/core/build_steps/compile.rs:2332
compile::Std { target: x86_64-unknown-linux-gnu, build_compiler: Compiler { stage: 1, host: x86_64-unknown-linux-gnu, forced_compiler: false }, crates: [], force_recompile: false, extra_rust_args: [], is_for_mir_opt_tests: false } at src/bootstrap/src/core/build_steps/compile.rs:1080
Build completed unsuccessfully in 0:09:56
---
[2026-07-11T13:36:40.309Z INFO  opt_dist::utils] Free disk space: 1.27 TiB out of total 2.51 TiB (49.21% used)
Error: Optimized build pipeline has failed

Caused by:
    Command RUST_BACKTRACE=full python3 /checkout/x.py build --target x86_64-unknown-linux-gnu --host x86_64-unknown-linux-gnu --stage 2 library/std --set rust.llvm-bitcode-linker=false --set build.extended=false --set rust.codegen-backends=['llvm'] --set rust.deny-warnings=false --set pgo.rustc.generate="/tmp/tmp-multistage/opt-artifacts/rustc-pgo" --set llvm.thin-lto=false --set llvm.link-shared=true [at /checkout/obj] has failed with exit code Some(1)

Stack backtrace:
   0: <anyhow::Error>::msg::<alloc::string::String>
             at /rust/deps/anyhow-1.0.102/src/backtrace.rs:10:14
   1: <opt_dist::exec::CmdBuilder>::run
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/exec.rs:81:17
   2: <opt_dist::exec::Bootstrap>::run
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/exec.rs:209:18
   3: opt_dist::execute_pipeline::{closure#1}::{closure#0}
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/main.rs:256:21
   4: <opt_dist::timer::TimerSection>::section::<opt_dist::execute_pipeline::{closure#1}::{closure#0}, ()>
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/timer.rs:111:22
   5: opt_dist::execute_pipeline::{closure#1}
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/main.rs:245:15
   6: <opt_dist::timer::TimerSection>::section::<opt_dist::execute_pipeline::{closure#1}, opt_dist::training::RustcPGOProfile>
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/timer.rs:111:22
   7: opt_dist::execute_pipeline
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/main.rs:242:35
   8: opt_dist::main
             at /rustc/bb887d8ddb734d012807b54778bfbed7a92abfee/src/tools/opt-dist/src/main.rs:469:18
   9: <fn() -> core::result::Result<(), anyhow::Error> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/core/src/ops/function.rs:250:5
  10: std::sys::backtrace::__rust_begin_short_backtrace::<fn() -> core::result::Result<(), anyhow::Error>, core::result::Result<(), anyhow::Error>>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/sys/backtrace.rs:166:18
  11: std::rt::lang_start::<core::result::Result<(), anyhow::Error>>::{closure#0}
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/rt.rs:206:18
  12: <&dyn core::ops::function::Fn<(), Output = i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe as core::ops::function::FnOnce<()>>::call_once
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/core/src/ops/function.rs:287:21
  13: std::panicking::catch_unwind::do_call::<&dyn core::ops::function::Fn<(), Output = i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe, i32>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:581:40
  14: std::panicking::catch_unwind::<i32, &dyn core::ops::function::Fn<(), Output = i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:544:19
  15: std::panic::catch_unwind::<&dyn core::ops::function::Fn<(), Output = i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe, i32>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panic.rs:359:14
  16: std::rt::lang_start_internal::{closure#0}
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/rt.rs:175:24
  17: std::panicking::catch_unwind::do_call::<std::rt::lang_start_internal::{closure#0}, isize>
             at /rustc/0417c25868d6dfbd1c291dfeae950504faa6f790/library/std/src/panicking.rs:581:40

_ => [usize; 2],
};

pub unsafe fn memchr(

@RalfJung RalfJung Jul 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation does not implement the contract specified for memchr by the C standard. That standard says:

This function behaves as if it reads the bytes sequentially and stops as soon as a matching bytes is found: if the array pointed to by ptr is smaller than count, but the match is found within the array, the behavior is well-defined.

This means we are not allowed to read any bytes beyond the first byte that is different, which means that the naive implementation is the only legal implementation (unless we use something like speculative loads which are still being worked on on the LLVM side: llvm/llvm-project#179642).

We can of course have our own contract for our own function, but we should make sure this never gets used by anything that expects libc::memchr.

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "as if" here is doing some heavy lifting. Otherwise glibc's implementation is also non-conformant according to your interpretation. (If I'm understanding what you're saying correctly.)

@RalfJung RalfJung Jul 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"as if" means "must not have more UB than".

So yes I am saying that if the glibc implementation reads bytes beyond the first occurrence of the needle (and it doesn't use inline asm to do so) then it is non-conformant, at least if it ends up in the same translation unit as the C program using it and hence the compiler can see that more bytes than "until the first difference" are being accessed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm getting lost here. Can you rephrase what you're saying more concretely? What I'm hearing from you is that the only correct implementation of memchr is the naive implementation, but this seems like a whacky state of affairs right? And if glibc's implementation is also non-conformant, but is used as a gold standard implementation of memchr (IMO it is at least), then I think it should be fine for us to use the same implementation tricks?

I think I'm not getting where this implementation specifically violates what the C standard is. I can't tell if your commentary is applying to all possible implementations of memchr beyond the naive one, or if it's something specific about the code in front of us here. So I think being more concrete here would be edifying.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe all such implementations in glibc are in fully separate .S files, so their actual behavior is not visible to the C compiler. At that level, it only needs to care about more direct hardware constraints, like making sure any possible over-reads are still in the same page as the matching byte, so it doesn't cause a rogue segfault. e.g. A fully aligned SIMD read will also be page-aligned, so that's fine.

@BurntSushi BurntSushi Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But there is a difference between "reading bytes beyond where s + n ends" and "reading bytes beyond the first occurence of c in s up to s + n." It sounds like @RalfJung is talking about the latter, in which case, I don't see why it's relevant whether Assembly is used or not. (I absolutely acknowledge that writing the impl in Assembly is relevant to the former case, and @cuviper is absolutely correct there. I just don't think we're talking about that case here.)

Like, is this implementation here committing UB somewhere? It might read past the first occurrence of the needle, but that doesn't seem like an issue to me. And that's where the "as if" does some heavier lifting I think.

I would really like to see someone be concrete about why this implementation is problematic. :-) Mostly because if it is, then there is perhaps something problematic about the memchr crate too. And perhaps all non-naive implementations of memchr. To be doubly clear, I find this statement to be controversial:

which means that the naive implementation is the only legal implementation

@RalfJung RalfJung Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rephrase what you're saying more concretely?

I am saying that this code is entirely well-defined:

#include <string.h>

int main() {
    char *haystack = "abcdef";
    memchr(haystack, 'f', 25600); // note that the `n` parameter is way bigger than the buffer
    return 0;
}

And the same goes for the equivalent Rust code invoking libc::memchr.
However, if memchr is implemented by doing 8-byte reads (such as the implementation in this PR), it would read beyond the bounds of haystack.

Do we have consensus on those two claims? If yes, then let's talk about the consequences.

OOB reads are usually UB. Whether this specific OOB read is UB depends on whether memchr itself is subject to the rules of the Abstract Machine. If it is written in Rust or C, then I would say that it is, though in the absence of LTO one can make complicated arguments about separate translation units. If it is written in assembly, and if the implementation is careful not to cause any segfaults by accessing any pages beyond the one that has the first hit, then that assembly code has a story that justifies its correctness.

So this Rust-written implementation here is not a valid replacement for a libc's memchr, unless we want to make complicated arguments about separate translation units and rely on no-LTO. I don't know if we want it to be a valid replacement for libc's memchr, but if not, then that should be clearly documented and we should be sure it doesn't accidentally get used as such.

I would really like to see someone be concrete about why this implementation is problematic. :-) Mostly because if it is, then there is perhaps something problematic about the memchr crate too.

I don't think there is a problem for the memchr crate unless it is being used as a replacement for libc::memchr. For a pure Rust function it is entirely reasonable to demand that the entire haystack of size n must be dereferenceable memory. But the C function happens to be specified in a way that says that haystack only has to be dereferenceable up until the first occurrence of needle -- a spec we'd never use in Rust, but we're not in charge of defining the spec of the C function.

@joboet joboet Jul 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because if it is, then there is perhaps something problematic about the memchr crate too.

Given that your crate makes no attempt to be identical to C's memchr, no. This whole thing is not a problem for functions that take a slice, as then the entire memory region has to be valid anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I get it now. I was not thinking about whacky values of n here. Thank you for the explanation @RalfJung!

Makes me wonder whether musl's implementation here is not correct? Since it's written in C and can read in word-sized chunks: https://github.com/kraj/musl/blob/a42e9dee266f398026a33d0793c66225c7997755/src/string/memchr.c#L15-L24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't easily make sense of what that code does but based on your description -- yes that sounds like it'd be not correct, modulo the "separate translation unit" argument.

// These targets have efficient SIMD acceleration.
any(
all(target_arch = "x86_64", target_feature = "sse2"),
all(target_arch = "aarch64", target_feature = "neon"),

@BurntSushi BurntSushi Jul 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the specific blocker to using the memchr crate here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use memchr in a normal fashion as it depends on core, which would create a circular dependency. It is possible to solve that, just like how backtrace is used in std even though it depends on std, but doing so is very complicated (backtrace is included as a git subtree and then #[path]-imported as a module from std. This requires a lot of cooperation on the part of backtrace, as it has to both compile as a crate and as a module in a bigger crate.) Hence I wanted to avoid that whole mess...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked the codegen for neon here?

Yes. first_set is implemented without movemask on all non-SSE-platforms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-compiler-builtins Area: compiler-builtins (https://github.com/rust-lang/compiler-builtins) I-libs-nominated Nominated for discussion during a libs team meeting. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. S-waiting-on-perf Status: Waiting on a perf run to be completed. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants