Skip to content

Conversation

@folkertdev
Copy link
Contributor

tracking issue: #146941
acp: rust-lang/libs-team#638

well, we don't expose prefetch_write_instruction, that one doesn't really make sense in practice.

The implementation is straightforward, the docs can probably use some tweaks. Especially for the instruction version it's a little awkward.

r? @Amanieu

@rustbot rustbot added 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 Sep 23, 2025
@rust-log-analyzer

This comment has been minimized.

/// Passing a dangling or invalid pointer is permitted: the memory will not
/// actually be dereferenced, and no faults are raised.
#[unstable(feature = "hint_prefetch", issue = "146941")]
pub const fn prefetch_read_instruction<T>(ptr: *const T, locality: Locality) {
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't this be ptr: unsafe fn() or something since some platforms have different data and instruction pointer sizes?

Copy link
Member

Choose a reason for hiding this comment

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

On some platforms a function pointer doesn't point directly to the instruction bytes, but rather to a function descriptor, which consists of a pointer to the first instruction and some value that needs to be loaded into a register. On these platforms using unsafe fn() would be incorrect. Itanium is an example, but I know there are more architectures that do this.

Copy link
Member

Choose a reason for hiding this comment

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

ok, but that doesn't mean *const T is correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ultimately all you need is an address, so *const T seemed the simplest way of achieving that.

Copy link
Member

@programmerjake programmerjake Sep 24, 2025

Choose a reason for hiding this comment

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

but *const T may be too small e.g. on 16-bit x86 in the medium model a data pointer is 16 bits but an instruction pointer is 32 bits.

there are some AVR cpus (not currently supported by rust?) which need >16 bits for instruction addresses but not for data, so they might have the same issue https://en.wikipedia.org/wiki/Atmel_AVR_instruction_set#:~:text=Rare)%20models%20with,zero%2Dextended%20Z.)

Copy link
Contributor Author

@folkertdev folkertdev Sep 25, 2025

Choose a reason for hiding this comment

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

Does that ACP actually use the LLVM address spaces? It's not really clear from the design. Also it looks like it was never actually nominated for T-lang?

Copy link
Member

Choose a reason for hiding this comment

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

LLVM address space usage is dictated by the target, that ACP doesn't use non-default address-spaces because for all existing targets a NonNull<Code> is sufficient for function addresses (AVR just uses 16-bit pointers for both code and data and AFAIK LLVM doesn't currently support >16-bit pointers), however the plan is to add a type BikeshedFnAddr and switch to using that whenever we add a target where that's insufficient.

Copy link
Member

Choose a reason for hiding this comment

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

AVR does use ptr addrspace(1) for function pointers: https://rust.godbolt.org/z/3hGPfKvfG

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@programmerjake do you see that ACP moving forward? Maybe I should remove the instruction prefetching for now here and add it when there is progress?

Copy link
Member

Choose a reason for hiding this comment

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

if you need just Code, you can probably get away with just adding that extern type for now under the tracking issue I just created #148768 for that ACP and let whoever implements the rest of that ACP just use Code. You can add them all now and wait on that tracking issue for stabilization. If it takes too long, this feature can be partially stabilized and leave the code prefetch stabilization for later.

@Amanieu
Copy link
Member

Amanieu commented Sep 24, 2025

After thinking about this for a bit, NonTemporal should be separated into a separate Retention enum since it is orthogonal to the locality at which to prefetch the data. Specifically:

  • "locality" refers to how soon we are going to need this data. This corresponds to the cache level into which we are prefetching.
  • "retention" refers to how long the data should be kept in cache. A non-temporal access includes a hint to the cache that the line should be evicted before any other cache lines. Non-temporal hints indicate memory that is accessed only once, after which it should not be kept in the cache any more.

So I would rework the API to something like this:

#[non_exhaustive]
pub enum Locality {
    L1,
    L2,
    L3,
}

#[non_exhaustive]
pub enum Retention {
    Normal,
    NonTemporal,
}

pub const fn prefetch_read_data<T>(ptr: *const T, locality: Locality, retention: Retention);

Even though not all of these map to the underlying LLVM intrinsic today, they may do so in the future.

/// Passing a dangling or invalid pointer is permitted: the memory will not
/// actually be dereferenced, and no faults are raised.
#[unstable(feature = "hint_prefetch", issue = "146941")]
pub const fn prefetch_write_data<T>(ptr: *mut T, locality: Locality) {
Copy link
Member

@bjorn3 bjorn3 Sep 24, 2025

Choose a reason for hiding this comment

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

Maybe make Locality a const generic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Enums cannot be const-generic parameters at the moment (on stable, anyway). We model the API here after atomic operations where the ordering parameter behaves similarly.

@folkertdev
Copy link
Contributor Author

Even though not all of these map to the underlying LLVM intrinsic today, they may do so in the future.

Maybe my understanding of NonTemporal is wrong, but I believe it means that the cache hierarchy should be skipped entirely. So then combining that with a Locality is completely meaningless, right?

It can be implemented (we'd just ignore weird/invalid combinations, I guess) but from an API perspective it seems weird.

@Amanieu
Copy link
Member

Amanieu commented Sep 24, 2025

No, non-temporal is a hint that the data is likely only going to be accessed once. Essentially if you have data that you're only reading once then you'll want to prefetch it all the way to L1, but then mark that cache line as the first that should be evicted if needed since you know it won't be needed in the future. See https://stackoverflow.com/questions/53270421/difference-between-prefetch-and-prefetchnta-instructions for details of how this works on x86 CPUs.

@folkertdev
Copy link
Contributor Author

So that means something like this?

#[inline(always)]
#[unstable(feature = "hint_prefetch", issue = "146941")]
pub const fn prefetch_read_data<T>(ptr: *const T, locality: Locality, retention: Retention) {
    match retention
        Retention::NonTemporal => {
            return intrinsics::prefetch_read_data::<T, { Retention::NonTemporal as i32 }>(ptr);
        }
        Retention::Normal => { /* fall through */ }
    }

    match locality {
        Locality::L3 => intrinsics::prefetch_read_data::<T, { Locality::L3 as i32 }>(ptr),
        Locality::L2 => intrinsics::prefetch_read_data::<T, { Locality::L2 as i32 }>(ptr),
        Locality::L1 => intrinsics::prefetch_read_data::<T, { Locality::L1 as i32 }>(ptr),
    }
}

This is really tricky to document: users basically have to look at the implementation to see what happens exactly. Also, every call getting the additional retention parameter is kind of unfortunate.

@Amanieu
Copy link
Member

Amanieu commented Sep 26, 2025

My main concern is that the cache level to prefetch into should not be mixed with the retention hint. It should be a separate parameter or a separate function altogether.

@folkertdev
Copy link
Contributor Author

In that case I think, given current hardware support at least, that a separate function would be better

#[inline(always)]
#[unstable(feature = "hint_prefetch", issue = "146941")]
pub const fn prefetch_read_data<T>(ptr: *const T, locality: Locality) {
    match locality {
        Locality::L3 => intrinsics::prefetch_read_data::<T, { Locality::L3 as i32 }>(ptr),
        Locality::L2 => intrinsics::prefetch_read_data::<T, { Locality::L2 as i32 }>(ptr),
        Locality::L1 => intrinsics::prefetch_read_data::<T, { Locality::L1 as i32 }>(ptr),
    }
}

#[inline(always)]
#[unstable(feature = "hint_prefetch", issue = "146941")]
pub const fn prefetch_read_data_nontemporal<T>(ptr: *const T) {
    return intrinsics::prefetch_read_data::<T, { Retention::NonTemporal as i32 }>(ptr);
}

that does potentially close some doors for weird future hardware designs, but as a user I think separate functions are simpler.

@rustbot

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@Amanieu
Copy link
Member

Amanieu commented Oct 5, 2025

The reason I argued for a separate argument is that it's possible LLVM will add support for specifying a cache level for non-temporal prefetches in the future. It also makes the API more symmetrical.

Alternatively, we could also decide to only expose prefetch hints with no extra arguments and point people to platform-specific hints in std::arch for more detailed hints.

@folkertdev
Copy link
Contributor Author

How heavily should we weigh potential future LLVM additions? Apparently no current architecture provides the fine-grained control of picking the cache level for non-temporal reads. So we're trading additional complexity for everyone versus a hypothetical future CPU capability.

Also, we've gotten this far without prefetching at all. I suspect that in practice the vast majority of uses will just be "load into L1", perhaps with some "load into L2". The heavily specialized stuff can probably just be left to stdarch.

The current implementation of this PR is to have

pub const fn prefetch_read_data<T>(ptr: *const T, locality: Locality);
pub const fn prefetch_read_data_nontemporal<T>(ptr: *const T);

I've left out the non-temporal variants for write and read_instruction for now, from what I can tell those don't actually seem that useful and can probably be left to stdarch unless someone does have an actual use case.

@programmerjake
Copy link
Member

programmerjake commented Oct 5, 2025

for streaming writes where you're unlikely to access the written data again in the near future, prefetch_write_data_nontemporal seems useful, at least it doesn't have crazy semantics like nontemporal stores do.

@programmerjake
Copy link
Member

also, for naming, imo we should leave out _data since that's likely waay more common than _instruction so makes a good default.

@Amanieu
Copy link
Member

Amanieu commented Oct 5, 2025

How heavily should we weigh potential future LLVM additions? Apparently no current architecture provides the fine-grained control of picking the cache level for non-temporal reads. So we're trading additional complexity for everyone versus a hypothetical future CPU capability.

AArch64 has this capability, see https://developer.arm.com/documentation/ddi0596/2021-06/Base-Instructions/PRFM--immediate---Prefetch-Memory--immediate--

@folkertdev
Copy link
Contributor Author

for streaming writes where you're unlikely to access the written data again in the near future, prefetch_write_data_nontemporal seems useful, at least it doesn't have crazy semantics like nontemporal stores do.

Can't you just do the non-temporal store? what benefit does a prefetch provide here?

also, for naming, imo we should leave out _data since that's likely waay more common than _instruction so makes a good default.

Yeah I had been thinking that too, I'll change that.

AArch64 has this capability

You can encode it in the instruction, I haven't been able to figure out whether it actually does anything in practice.

We can add the locality argument to the non-temporal function(s) though, I'd be OK with that given that non-temporal is even more niche than standard prefetches.

@programmerjake
Copy link
Member

programmerjake commented Oct 5, 2025

Can't you just do the non-temporal store?

non-temporal stores break the memory model on x86: llvm/llvm-project#64521 and #114582

@folkertdev
Copy link
Contributor Author

And then the idea is that a non-temporal prefetch write hint plus a standard write will in effect create a well-behaved non-temporal store?

@programmerjake
Copy link
Member

And then the idea is that a non-temporal prefetch write hint plus a standard write will in effect create a well-behaved non-temporal store?

maybe, depending on the arch? it at least won't break the memory model.

@Amanieu
Copy link
Member

Amanieu commented Nov 9, 2025

I'm not too concerned about the pointer type for prefetch_read_instruction: we can document this as a pointer into the data address space and simply treat it as a nop if this is disjoint from the code address space. In any case, we can defer stabilization of this until the issues of code vs data pointer size is resolved by lang.

I'm much more concerned about the locality and retention arguments and increasingly think we should not accept those parameters on the generic intrinsic given that proper use of those requires a lot of hardware specific knowledge. I would prefer to point people to the arch-specific intrinsics in stdarch if they need this level of precision.

@folkertdev
Copy link
Contributor Author

I'm much more concerned about the locality and retention arguments and increasingly think we should not accept those parameters on the generic intrinsic given that proper use of those requires a lot of hardware specific knowledge. I would prefer to point people to the arch-specific intrinsics in stdarch if they need this level of precision.

I'd agree for retention, but I think locality can be exposed. At least in the zstd case, both (what we here call) L1 and L2 are used, and so I'd like a cross-platform function that can do that. Sure you do need some low-level knowledge to use these options effectively, but I think the mental model is reasonably clear.

@Amanieu Amanieu added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Dec 9, 2025
Comment on lines 840 to 848
L3 = 1,
/// Data is expected to be reused in the near future.
///
/// Typically prefetches into L2 cache.
L2 = 2,
/// Data is expected to be reused very soon.
///
/// Typically prefetches into L1 cache.
L1 = 3,
Copy link
Member

Choose a reason for hiding this comment

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

Could you remove the integer values from the enum? These are internal implementation details and not something that we want to publicly expose.

@Amanieu Amanieu removed the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Dec 9, 2025
@the8472
Copy link
Member

the8472 commented Dec 9, 2025

At least in the zstd case, both (what we here call) L1 and L2 are used, and so I'd like a cross-platform function that can do that

Are CPUs consistent enough across vendors and generations that using the same hints in the same places is useful on all of them? Naively I'd expect that with different memory latencies, pipeline depths, different cache-line sizes and prefetcher differences they'd need bespoke optimizations. Has anyone benchmarked that?
I peeked at the git history from zstd and the one PR I looked at (facebook/zstd#2749) did several things and has benchmarks across multiple CPUs, but it didn't seem to benchmark the prefetching in isolation.

@rustbot
Copy link
Collaborator

rustbot commented Dec 9, 2025

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@Amanieu
Copy link
Member

Amanieu commented Dec 9, 2025

@bors r+

@bors
Copy link
Collaborator

bors commented Dec 9, 2025

📌 Commit b9e3e41 has been approved by Amanieu

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 9, 2025
@bors
Copy link
Collaborator

bors commented Dec 10, 2025

⌛ Testing commit b9e3e41 with merge 2e667b0...

@bors
Copy link
Collaborator

bors commented Dec 10, 2025

☀️ Test successful - checks-actions
Approved by: Amanieu
Pushing 2e667b0 to main...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Dec 10, 2025
@bors bors merged commit 2e667b0 into rust-lang:main Dec 10, 2025
12 checks passed
@rustbot rustbot added this to the 1.94.0 milestone Dec 10, 2025
@github-actions
Copy link
Contributor

What is this? This is an experimental post-merge analysis report that shows differences in test outcomes between the merged PR and its parent PR.

Comparing 5f1173b (parent) -> 2e667b0 (this PR)

Test differences

Show 2 test diffs

2 doctest diffs were found. These are ignored, as they are noisy.

Test dashboard

Run

cargo run --manifest-path src/ci/citool/Cargo.toml -- \
    test-dashboard 2e667b0c6491678642a83e3aff86626397360af5 --output-dir test-dashboard

And then open test-dashboard/index.html in your browser to see an overview of all executed tests.

Job duration changes

  1. dist-aarch64-apple: 6723.7s -> 7889.0s (+17.3%)
  2. x86_64-gnu-llvm-20-3: 5726.8s -> 6693.8s (+16.9%)
  3. x86_64-gnu-gcc: 2966.0s -> 3382.5s (+14.0%)
  4. x86_64-rust-for-linux: 2757.2s -> 3119.7s (+13.1%)
  5. x86_64-gnu-llvm-20: 2433.7s -> 2750.6s (+13.0%)
  6. x86_64-gnu-tools: 3217.4s -> 3620.1s (+12.5%)
  7. dist-i586-gnu-i586-i686-musl: 5611.9s -> 4948.1s (-11.8%)
  8. aarch64-gnu-llvm-20-2: 2194.4s -> 2442.7s (+11.3%)
  9. i686-gnu-2: 5511.1s -> 6133.1s (+11.3%)
  10. aarch64-gnu-debug: 3832.5s -> 4264.3s (+11.3%)
How to interpret the job duration changes?

Job durations can vary a lot, based on the actual runner instance
that executed the job, system noise, invalidated caches, etc. The table above is provided
mostly for t-infra members, for simpler debugging of potential CI slow-downs.

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (2e667b0): comparison URL.

Overall result: ✅ improvements - no action needed

@rustbot label: -perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-0.5% [-0.5%, -0.5%] 1
All ❌✅ (primary) - - 0

Max RSS (memory usage)

Results (primary 0.6%, secondary -2.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.6% [0.6%, 0.6%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.2% [-2.2%, -2.2%] 1
All ❌✅ (primary) 0.6% [0.6%, 0.6%] 1

Cycles

Results (primary 2.5%, secondary -0.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.5% [2.5%, 2.5%] 2
Regressions ❌
(secondary)
2.0% [2.0%, 2.0%] 1
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.4% [-2.4%, -2.4%] 1
All ❌✅ (primary) 2.5% [2.5%, 2.5%] 2

Binary size

This benchmark run did not return any relevant results for this metric.

Bootstrap: 471.137s -> 469.7s (-0.31%)
Artifact size: 389.02 MiB -> 388.99 MiB (-0.01%)

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

Labels

merged-by-bors This PR was explicitly merged by bors. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. 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