Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add std::thread::available_concurrency #74480

Merged
merged 1 commit into from Oct 18, 2020
Merged

Conversation

@yoshuawuyts
Copy link
Member

@yoshuawuyts yoshuawuyts commented Jul 18, 2020

This PR adds a counterpart to C++'s std::thread::hardware_concurrency to Rust, tracking issue #74479.

cc/ @rust-lang/libs

Motivation

Being able to know how many hardware threads a platform supports is a core part of building multi-threaded code. In C++ 11 this has become available through the std::thread::hardware_concurrency API. Currently in Rust most of the ecosystem depends on the num_cpus crate (no.35 in top 500 crates) to provide this functionality. This PR proposes an API to provide access to the number of hardware threads available on a given platform.

edit (2020-07-24): The purpose of this PR is to provide a hint for how many threads to spawn to saturate the processor. There's value in introducing APIs for NUMA and Windows processor groups, but those are intentionally out of scope for this PR. See: #74480 (comment).

Naming

Discussing the naming of the API on Zulip surfaced two options:

  • std::thread::hardware_concurrency
  • std::thread::hardware_threads

Both options seemed acceptable, but overall people seem to gravitate the most towards hardware_threads. Additionally @jonas-schievink pointed out that the "hardware threads" terminology is well-established and is used in among other the RISC-V specification (page 20):

A component is termed a core if it contains an independent instruction fetch unit. A RISC-V-compatible core might support multiple RISC-V-compatible hardware threads, or harts, through multithreading.

It's also worth noting that the original paper introducing C++'s std::thread submodule unfortunately doesn't feature any discussion on the naming of hardware_concurrency, so we can't use that to help inform our decision here.

Return type

An important consideration @joshtriplett brought up is that we don't want to default to 1 for platforms where the number of available threads cannot be retrieved. Instead we want to inform the users of the fact that we don't know and allow them to handle that case. Which is why this PR uses Option<NonZeroUsize> as its return type, where None is returned on platforms where we don't know the number of hardware threads available.

The reasoning for NonZeroUsize vs usize is that if the number of threads for a platform are known, they'll always be at least 1. As evidenced by the example the NonZero* family of APIs may currently not be the most ergonomic to use, but improving the ergonomics of them is something that I think we can address separately.

Implementation

@Mark-Simulacrum pointed out that most of the code we wanted to expose here was already available under libtest. So this PR mostly moves the internal code of libtest into a public API.

@rust-highfive
Copy link
Collaborator

@rust-highfive rust-highfive commented Jul 18, 2020

r? @dtolnay

(rust_highfive has picked a reviewer for you, use r? to override)

@luser
Copy link
Contributor

@luser luser commented Jul 18, 2020

To clarify, would this report the number of "logical" cores on CPUs that support hyperthreading? That's probably the right choice if the API intends to answer the question "how much parallelism is reasonable".

@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Jul 18, 2020

To clarify, would this report the number of "logical" cores on CPUs that support hyperthreading?

@luser Yes I believe it should. Since I didn't write the backing impl I had to verify, but the num_cpus::get implementation takes an identical approach as the code in rustc (as opposed to the num_cpus::get_physical).

Perhaps we should clarify in the docs what we mean by "hardware threads"?

@dfamonteiro
Copy link

@dfamonteiro dfamonteiro commented Jul 18, 2020

To clarify, would this report the number of "logical" cores on CPUs that support hyperthreading? That's probably the right choice if the API intends to answer the question "how much parallelism is reasonable".

That is a great point. Related to this, is it worthwhile to have two functions that distinguish between the number of logical cores and the number of hardware cores?

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from b2e2c28 to 129d6d8 Jul 18, 2020
@ehuss
Copy link
Contributor

@ehuss ehuss commented Jul 18, 2020

Would you mind retaining the original code that used cfg_if? It intentionally was written with a fall-through.

Also, my impression is that all platform-specific code should be in the sys module. I'm not sure how much that is enforced, though.

Might also want to address, the num_cpus code has been more maintained and supports more than this does. Any particular reason not to use that?

@mati865
Copy link
Contributor

@mati865 mati865 commented Jul 18, 2020

Hardware threads is correct term but can be quite confusing for some people. To make it easier you can view it as:

  • software threads - created by app inside OS, is not related to amount of CPU threads
  • hardware threads - in a great simplification: how many threads CPU can handle simultaneously (for SMT CPUs it's cores*<threads per core>)
  • cores - physical cores, SMT threads are not included
@guswynn
Copy link
Contributor

@guswynn guswynn commented Jul 18, 2020

This doesn't seem to have support for target_os="none", unless I'm totally missing how cfg(target_os) works?

@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Jul 18, 2020

What @mati865 says is accurate. Running the num_cpus APIs on my AMD computer yields the following results:

fn main() {
    println!("hardware threads {}", num_cpus::get());
    println!("hardware cores {}", num_cpus::get_physical());
}
hardware threads: 24
hardware cores: 12

@dfamonteiro I'd like to keep the scope of this PR minimal; but it's certainly possible an API along the lines of std::thread::hardware_cores could be introduced in the future. One takeaway I'm having from this conversion so far is that we need to do a better job at explaining what a "hardware thread" in the documentation.

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from 129d6d8 to 8b93bcd Jul 18, 2020
@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Jul 18, 2020

@ehuss ah yes, apologies -- seems I missed that in the rebase. Fixed it!

@guswynn it now does (:

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from 8b93bcd to a548bfb Jul 18, 2020
@the8472
Copy link
Contributor

@the8472 the8472 commented Jul 18, 2020

What @mati865 says is accurate. Running the num_cpus APIs on my AMD computer yields the following results:

fn main() {
    println!("hardware threads {}", num_cpus::get());
    println!("hardware cores {}", num_cpus::get_physical());
}
hardware threads: 24
hardware cores: 12

num_cpus does not actually tell you the number of hardware threads, it tells you the maximum scheduling capacity available to the current process, which can be lower than the number of hardware threads if CPU affinities, cgroups or similar things are applied. This probably is what most thread pools actually want.

Returning the number of hardware threads would lead to oversubscription in containers.

Java does the same: https://www.docker.com/blog/improved-docker-container-integration-with-java-10/

@SimonSapin
Copy link
Contributor

@SimonSapin SimonSapin commented Jul 18, 2020

None is returned on platforms where we don't know the number of hardware threads available.

This Option::None value represents not the absence of hardware thread, but a failure to retrieve information about them. It should therefore be Result::Err(something) instead.

we don't want to default to 1 for platforms where the number of available threads cannot be retrieved

For the same reason, it seems undesirable to return 1 on supported platforms when the corresponding libc call returns an error. Why not return also return Err(something) in those situations? std::io::Error might be appropriate.

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch 4 times, most recently from 6e49069 to ba6c552 Jul 18, 2020
@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Jul 18, 2020

Updated with @SimonSapin and Leo Le Boueter's feedback; we now return io::Result<NonZeroUsize> because syscalls may fail and we should forward the error.

@retep998
Copy link
Member

@retep998 retep998 commented Jul 18, 2020

There needs to be a loud section about the limitations of this API on Windows. Specifically that it only returns the number of logical processors in the current processor group which is limited to at most 64 logical processors. To support more than 64 logical processors requires explicit support for enumerating processor groups and assigning threads to logical processors in other groups.

https://docs.microsoft.com/en-us/windows/win32/procthread/processor-groups

@joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Jul 19, 2020

@retep998 Rather than having that limitation, we should be calling the APIs that let us figure out the total number of hardware threads, in all processor groups.

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from ba6c552 to b75ad69 Jul 19, 2020
@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Jul 19, 2020

@joshtriplett @retep998 Added an entry on the tracking issue under "known issues" that the amount of threads reported on Windows is at most 64. Also tracking @luser's report that the interaction with CPU affinity on Linux doesn't seem right.

@mati865
Copy link
Contributor

@mati865 mati865 commented Jul 19, 2020

@joshtriplett and place warning that users will be limited to 64 hardware threads unless they apply Windows specific workaround. I suppose they will rather use crate that does it for them...

@retep998
Copy link
Member

@retep998 retep998 commented Jul 19, 2020

@joshtriplett Threads will not run on other processor groups by default. Normally threads only have access to the logical processors in the processor group for that process, so std::thread::hardware_threads should not count logical processors in other groups. If we want to count logical processors outside the current processor group, then we need to provide a more comprehensive API that reports information on processor groups and NUMA nodes and CPU sets, and have APIs to assign threads appropriately.

@joshtriplett
Copy link
Member

@joshtriplett joshtriplett commented Jul 19, 2020

@retep998 Is it possible for a thread's affinity to be for multiple processor groups at once, or does a thread have to be limited to a single processor group at a time no matter how it's started or modified? If the former, we could use that and start threads that can run anywhere by default. If the latter, do Rayon and other libraries use the appropriate APIs at the moment to start threads on every hardware thread across the system?

Long-term, I'd like us to have NUMA-aware APIs as well, but for the moment, it'd be nice to have APIs for the simple case of "start as many hardware threads as the system has".

@bors
Copy link
Contributor

@bors bors commented Oct 16, 2020

@yoshuawuyts: 🔑 Insufficient privileges: not in try users

1 similar comment
@bors
Copy link
Contributor

@bors bors commented Oct 16, 2020

@yoshuawuyts: 🔑 Insufficient privileges: not in try users

@JohnTitor
Copy link
Member

@JohnTitor JohnTitor commented Oct 16, 2020

IIRC we don't have FreeBSD builder on the try build so let's just re-add this to the queue :)
@bors r=dtolnay

@bors
Copy link
Contributor

@bors bors commented Oct 16, 2020

📌 Commit 9508c32 has been approved by dtolnay

@ehuss
Copy link
Contributor

@ehuss ehuss commented Oct 16, 2020

@bors r-

This will still fail:

error[E0308]: mismatched types
   --> library/std/src/thread/available_concurrency.rs:115:27
    |
115 |                 if res == -1 {
    |                           ^^ expected `()`, found integer

error: aborting due to previous error

The semicolon in the unsafe block makes the res variable equal to ().

If you have Docker, I would encourage trying to test using that.

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Oct 16, 2020

Alternative you can use Miri:
XARGO_RUST_SRC=~/src/path/to/rust/library cargo miri setup --target xxx
This does a check-only build so no linker or build tools for the target are required. (This code does not seem to have any cfg(miri) in it either that could affect the result here. Directly using xargo would avoid that but that is a bit more work to set up.)

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from 9508c32 to 42a9706 Oct 16, 2020
@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Oct 16, 2020

The semicolon in the unsafe block makes the res variable equal to ().

@ehuss Ah yeah, good catch. I thought that followed out of an earlier error, but I got it wrong. That should be fixed now. Apparently I'd also missed that for OpenBSD which is fixed now too.

If you have Docker, I would encourage trying to test using that.

Do we have instructions on how to run this available? The Windows instructions for the compiler ended up being based on my notes, and researching that took a fair bit of work. If we don't have instructions on how to compile locally on BSD on Docker yet we should probably open up an issue for this somewhere (not sure if rust-lang/rust is the right repo for that?)

@ehuss
Copy link
Contributor

@ehuss ehuss commented Oct 16, 2020

Do we have instructions on how to run this available?

https://rustc-dev-guide.rust-lang.org/tests/intro.html#testing-with-docker-images

The command for freebsd is src/ci/docker/run.sh dist-x86_64-freebsd. Unfortunately there is no setup for openbsd. I have an openbsd VM I occasionally use for testing, but I have never built rustc on it, and I suspect it is not easy. Using miri also sounds useful, though I don't have much experience with that.

@yoshuawuyts
Copy link
Member Author

@yoshuawuyts yoshuawuyts commented Oct 16, 2020

@RalfJung that seemed to work successfully for both FreeBSD and OpenBSD! Though from the output it seems like it may only have checked core but not std? For posterity and anyone trying this on Windows, the way I did was using the following:

PS C:\Users\yoshu\Code\rust> rustup override set nightly-x86_64-pc-windows-gnu # miri doesn't seem to work yet on msvc
PS C:\Users\yoshu\Code\rust> rustup component add miri
PS C:\Users\yoshu\Code\rust> $env:XARGO_RUST_SRC='C:\Users\yoshu\Code\rust\library'
PS C:\Users\yoshu\Code\rust> cargo miri setup --target x86_64-unknown-freebsd
PS C:\Users\yoshu\Code\rust> cargo miri setup --target x86_64-unknown-openbsd

@ehuss dang, that seems to assume a Linux environment and I'm running on Windows. I don't think I'll be able to get that to work without provisioning a new environment (VM or otherwise) which I don't have the bandwidth for right now.

Given all known issues have been addressed at this point, can we give this a shot at running through bors again?

@yoshuawuyts yoshuawuyts force-pushed the yoshuawuyts:hardware_threads branch from 42a9706 to 3717646 Oct 16, 2020
@RalfJung
Copy link
Member

@RalfJung RalfJung commented Oct 17, 2020

that seemed to work successfully for both FreeBSD and OpenBSD! Though from the output it seems like it may only have checked core but not std?

It should check core, std, and even test. And the output looks like that for me:

$ cargo miri setup --target x86_64-unknown-freebsd
   Compiling compiler_builtins v0.1.35
    Checking core v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/core)
   Compiling libc v0.2.79
   Compiling cc v1.0.60
   Compiling std v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std)
   Compiling unwind v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/unwind)
    Checking rustc-std-workspace-core v1.99.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/rustc-std-workspace-core)
    Checking cfg-if v0.1.10
    Checking alloc v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/alloc)
    Checking rustc-demangle v0.1.16
    Checking panic_abort v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/panic_abort)
    Checking rustc-std-workspace-alloc v1.99.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/rustc-std-workspace-alloc)
    Checking panic_unwind v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/panic_unwind)
    Checking hashbrown v0.9.0
    Finished release [optimized] target(s) in 25.33s
    Checking rustc-std-workspace-std v1.99.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/rustc-std-workspace-std)
    Checking proc_macro v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/proc_macro)
    Checking term v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/term)
    Checking unicode-width v0.1.8
    Checking getopts v0.2.21
    Checking test v0.0.0 (/home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/test)
    Finished release [optimized] target(s) in 1.79s

(Ignore the "compiling", that's a cargo bug: rust-lang/cargo#7921)

@ehuss
Copy link
Contributor

@ehuss ehuss commented Oct 18, 2020

@bors r=dtolnay

@bors
Copy link
Contributor

@bors bors commented Oct 18, 2020

📌 Commit 3717646 has been approved by dtolnay

@bors
Copy link
Contributor

@bors bors commented Oct 18, 2020

Testing commit 3717646 with merge c38ddb8...

@bors
Copy link
Contributor

@bors bors commented Oct 18, 2020

☀️ Test successful - checks-actions, checks-azure
Approved by: dtolnay
Pushing c38ddb8 to master...

@bors bors added the merged-by-bors label Oct 18, 2020
@bors bors merged commit c38ddb8 into rust-lang:master Oct 18, 2020
12 checks passed
12 checks passed
PR (mingw-check, ubuntu-latest-xl)
Details
PR (x86_64-gnu-llvm-8, ubuntu-latest-xl)
Details
PR (x86_64-gnu-tools, 1, ubuntu-latest-xl)
Details
auto
Details
auto-fallible
Details
try
Details
master
Details
bors build finished
Details
bors build finished
Details
bors build finished
Details
bors build finished
Details
homu Test successful
Details
@rustbot rustbot added this to the 1.49.0 milestone Oct 18, 2020
bors added a commit to rust-lang/miri that referenced this pull request Oct 18, 2020
test new available_concurrency function

Cc rust-lang/rust#74480
@yoshuawuyts yoshuawuyts deleted the yoshuawuyts:hardware_threads branch Oct 18, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

You can’t perform that action at this time.