hardwareConcurrency
−
The Verification Venue · your own machine is the apparatus
Your browser will tell you how many processors you have. It is one integer, it arrives instantly, and the specification that defines it explicitly permits it to be wrong. Below, your machine is asked the same question in the only way that cannot be fudged: run a fixed amount of arithmetic, cut it into p pieces, and count the workers that were each worth a whole core.
The two numbers are not the same quantity and they are not usually the same integer. One is a policy decision inside a browser engine, and you can read the exact lines that make it. The other is a property of the silicon, published by the company that sold it to you, and the one API that exists is under no obligation to report it. Section 01 finds a single exception in three engines' source, on one platform, by a route that can silently fall back to the other number. Everything below is built out of that gap.
There is one API. It is navigator.hardwareConcurrency, and on this page load it says:
hardwareConcurrency
−
Engine this looks like
−
crossOriginIsolated
−
SharedArrayBuffer
−
Workload self test
−
The self test runs the shipped arithmetic over a fixed range, whole and cut seven ways, and checks the two agree. It is the same integer in every engine, which is what makes the rest of this page comparable across machines.
The standard is unusually candid about what that integer is worth. The HTML Living Standard
says the getter −
And then, in the same section:
−
Those are the two halves of the honest position: it is an upper bound with an escape clause. What that comes to in practice is decided in three files. Here they are, quoted from the engines’ own repositories, committed alongside this page.
| engine | what the getter actually does |
|---|
Read those three and the differences stop being subtle. Chromium reports what the operating system reports. Firefox reports the same, clamped by a preference whose shipped default is −, except on macOS, where it first asks the system for the physical core count and uses the operating system count only if that call is unavailable or returns zero. That branch is the one place any shipping browser hands over the quantity the rest of this page has to work to recover, and it is one engine on one platform. And WebKit does not report a count at all: it returns one of two constants, and the comment above the line has been asking for the number to be raised since machines with more than eight cores were rare.
This is the experiment that settles it. One computer, one moment, one browser process per cell, launched with a CPU affinity mask that hands it a known number of hardware threads. The question asked is only navigator.hardwareConcurrency. Nothing is timed.
| threads the process may use | Chromium | Firefox | WebKit |
|---|
−
Here is the whole method. Take one fixed amount of arithmetic. Give all of it to one worker and time it. Then cut the same job into two, three, up to as many pieces as your browser will give workers for, and time each. The speedup at p workers is the one worker time divided by the p worker time, and the marginal gain of the p-th worker is how much that speedup rose when it arrived, in units of one whole core.
A worker that landed on a core of its own contributes about 1. A worker that landed on the second thread of a core already in use contributes a fraction, because the two threads are sharing one set of execution units. A worker with nowhere to go contributes about 0. So count the workers that earned their place, in order, and you have counted cores.
This will use every processor you have, at full tilt, for the duration. Your fan may spin up. Stop ends it at once and prints nothing, and so does hiding this tab or leaving the page, because a page that holds your cores at full load in the background has stopped being a measurement. Nothing leaves the page either way: no timing is stored, sent or written down anywhere, and the arithmetic vanishes when you close the tab.
−
Your browser said
−
Workers worth a core
−
Best speedup seen
−
Job size chosen
−
Rate per iteration
−
Warm-up sped it up by
−
Timed runs kept
−
Worst drift in the sweep
−
The dotted line is one core per worker, which nothing can beat for long. The filled marker is where the count stopped.
| workers | fastest run | speedup | this worker added | efficiency | serial fraction e(p) | drift |
|---|
This cannot be measured here, and here is why
No core count is printed. A page that always produces a figure is a page that will eventually produce a wrong one, and on a shared or throttled machine this measurement genuinely does not hold.
The count above rests on the shape of the curve at small worker counts, and that shape depends on which processors your two workers landed on. A page cannot ask for a placement, cannot see the one it got, and on a machine with two threads per core the difference between the two answers is the whole finding: two workers on two different cores run twice as fast, and two workers on the two threads of a single core do not. Below are three passes of the identical measurement, taken one after another on a quiet machine.
| taken at | 2nd worker added | 3rd | 4th | best speedup | recovered | verdict |
|---|
−
−
If it printed a number, go and check it. Your CPU’s manufacturer publishes both figures for the exact part you own: Intel calls them Total Cores and Total Threads on ARK, AMD lists # of CPU Cores and # of Threads, and Apple states the performance and efficiency core counts in the technical specifications for each chip. That published integer is the anchor. It is not a number this page can see, which is the point: you are the only person in this transaction who can complete the check.
“Worth a core” needs a number. The rule above counts a worker if it raised the speedup by at least − of a core, and a threshold chosen by the person reporting the result is exactly the kind of thing that should make you suspicious. So here is the threshold on a slider, and underneath it every capture whose true answer is known independently, from the CPU affinity mask it was taken under.
Each row was captured with the process pinned to a known set of hardware threads, so its answer was known before any timing was taken. The rows marked held out were taken inside real browser engines and were used to choose nothing at all: the floor, and the window under it, come only from the unmarked rows. The marked ones are a test, and they are printed pass or fail.
| capture | what it was given | truth | recovered | gains per worker | verdict |
|---|
−
Every fitted capture correct for
−
Width of that window
−
Whole machine goes vacuous below
−
And collapses to one above
−
−
Both ends of that slider are traps, and naming them is the honest part. Drag it low enough and every worker passes, so the recovered count is always the browser’s own number and the comparison is true by construction: the page would agree with the API on every machine on earth and would have measured nothing. Drag it high enough and no worker ever passes, so every machine has one core. The rule is only worth anything in between, and the window is measured above rather than asserted.
Amdahl’s model says a job with a serial fraction f can never go faster than 1/f, however many processors you throw at it: S(p) = 1 / (f + (1-f)/p). Solve that for f at a single measured point and you get a per point estimate of the serial fraction, usually called the Karp–Flatt metric: e(p) = (1/S(p) − 1/p) / (1 − 1/p). It is one line of algebra and it is enormously more useful than the fitted f, because a rising e is not noise. It is the machine telling you that the p-th processor was not like the ones before it.
Each of these was taken with a CPU affinity mask that fixes the effective topology, so the shape of e(p) can be compared with an answer that was known in advance.
Given
−
Physical cores in that set
−
Recovered here
−
Pooled fit of f
−
Which claims a ceiling of
−
| p | fastest run | S(p) | added | e(p) | reading |
|---|
−
Now the reason the fitted f is not printed as a headline anywhere on this page. A serial fraction was injected into the same harness, on the same machine: a known share of the work was forced through one worker and the rest was split, and the fit was asked to recover the number it was given.
| f injected | S(2) | S(3) | S(4) | f fitted | error | cores recovered |
|---|
−
A reduction that returns a confident integer from nonsense is not an instrument. These are five deliberately broken measurements. Four are committed captures, taken on the machine that took the honest ones; the fifth kind is run live in your own browser when you press the button, so you can watch your machine produce the lie and watch the same reduction reject it.
| control | what is wrong with it | best speedup | fitted f | refused for | caught |
|---|
−
| control | best speedup here | work divided | refused for | caught |
|---|---|---|---|---|
| not run yet | ||||
The live controls use a short sweep, so their numbers are rougher than the committed ones. What has to hold is not the number but the verdict, and each row is asked for the same refusal its committed twin above got, read off that twin rather than typed in here. Note which rule catches the sleeper: it hands each worker a slice of the wait rather than the whole wait, so its speedup is about the number of workers and can never exceed the machine. No timing rule can dislike it. What catches it is the arithmetic, which no clock is involved in. The honest control is the other way round: if your machine is loaded or throttling it can be refused, and that row prints the reason instead of a verdict, because an instrument declining a bad moment is the instrument working.
The sharpest of them is the one where nothing is faked at all. In the racing control the work really is divided, every worker really does its slice, and the checksums really do add up. The only fault is in the driver: it stops its clock when the first worker answers instead of the last, and one slice is deliberately small. That is a real bug that real benchmark harnesses have, it produces a beautiful curve, and the arithmetic check cannot see it. Only the rule that speedup may not exceed the machine catches it.
Before any of the above could be trusted, one thing had to be true: the same arithmetic has to take the same time twice. It did not. The first workload here was an ordinary integer mixing loop, and inside a worker its compiled speed is not a number, it is a sequence.
Rejected kernel, fastest
−
Rejected kernel, slowest
−
Which is a spread of
−
Shipped kernel, spread
−
| kernel | fastest | slowest | spread | distinct levels | what it is |
|---|
−
A three-fold change in the speed of the thing being timed, arriving part way through, is larger than the whole effect this page measures. It cannot be averaged away and it cannot be warmed away in any reasonable time. The fix for an unstable instrument is a better instrument, not better statistics laid over the top of it, so the workload was replaced. The rejected loop is still in workload.mjs, exported and unused, because deleting it would hide how easily this page could have shipped a speedup curve made of compiler.
Replacing it was not enough, and the second half of this is worse than the first. A pool of workers does not compile in unison. Each one is its own runtime, tiering up on its own schedule, and for a while several of them sit at one speed while the rest sit at another. A sweep taken across a pool in that state compares a one worker run served by one compiler with a four worker run served by another, and calls the difference parallelism. Three consecutive passes of the identical measurement on this machine came back with recovered counts of one, one and two while that was happening.
So the warm-up does not count runs, and it does not trust an average. It probes every worker on its own, repeatedly, and refuses to start until they all agree about how fast this arithmetic is. Below is what that costs and what it buys, read off the committed captures themselves.
| capture | rounds | each worker, ns per iteration | spread | agreed |
|---|
−
Everything so far holds the job fixed and cuts it up, which is the arrangement Amdahl was arguing about. There is a second arrangement, usually named after Gustafson’s 1988 note: hold the work per worker fixed and let the job grow. Nobody buys a bigger computer to run the same small job faster; they run a bigger job.
The arithmetic is elementary and is worth doing in front of you rather than citing. Suppose the measured run spends a fraction s of its wall time in a serial phase and the rest in a parallel phase spread over N workers. One worker doing that same grown job would spend the same s, and N times the parallel part, so it would take s + N(1-s) times as long. That ratio is the scaled speedup, and unlike Amdahl’s it has no ceiling at all: it rises without bound in N.
| workers | wall time | one worker, same job | scaled speedup | s + N(1-s) | shortfall |
|---|
Measured at the widest
−
The law predicts
−
The gap
−
Physical cores on that machine
−
−
Both laws assume N equal, independent processors. The machine has not got N of those, and the shortfall is the same deficit the sweep found, arriving by a completely different route.
Amdahl’s 1967 note is three pages long, it is an argument about buying mainframes, and
the editors who reprinted it in 2007 noticed something worth repeating. Of the paper, they
wrote: −
The formula everybody calls Amdahl’s law is not in
Amdahl’s paper. What is in it is an estimate about housekeeping: data management
overhead, he says, −
, and
−
That five to seven times is the most quoted number in the paper and it is not a ceiling on parallel speedup, so it is not compared to anything on this page. It is a 1967 estimate of one specific overhead in one specific kind of workload, and setting a modern laptop’s measured speedup beside it would be numerology. The paper supplies the method and the question. The number to check your machine against is the one your own vendor prints. The sentence the whole law is really named for is the last one:
−
The falling curve is Amdahl: a fixed job, cut p ways, ceiling 1/f. The straight line is the weak scaling arithmetic above at the same f. Your measured points appear once you have run the sweep.
Amdahl ceiling at this f
−
Amdahl at p = 64
−
Weak scaling at N = 64
−
f that best fits your points
−
running…
Every figure above is recomputed from the same three modules the offline verifier imports. Rows marked RESTATEMENT only prove this page agrees with itself. Rows marked GROUND TRUTH, CONTROL and PROVENANCE are the ones that can actually go red, and rows marked OFFLINE: record something this browser cannot redo.
Something below is red. That means this page is wrong, not the check. The reduction, the committed captures and the quoted sources disagree with each other, and nothing printed above should be trusted until it is fixed.
Three modules, one copy each. workload.mjs holds the arithmetic that gets timed. sweep.mjs holds the measurement protocol: the adaptive warm-up, the sizing of the job, the interleaved ascending and descending passes. scaling.mjs holds every threshold, the speedup, the marginal gain, the Karp–Flatt metric, the pooled fit and every refusal rule. All three are imported without modification by this page, by the offline mirror (research/how-many-cores-do-you-have/09-verify.mjs) and by the headless check (verify-how-many-cores-do-you-have.mjs). The only thing that differs between the browser and the capture harness is a single injected function that hands chunks to workers: Web Workers here, node worker_threads there.
No SharedArrayBuffer is used and none is available: this site sends no Cross-Origin-Opener-Policy or Cross-Origin-Embedder-Policy headers, so crossOriginIsolated is false, which the panel at the top reports from your own browser. Message passing alone is enough. Real OS threads are real OS threads whether or not they share memory, and the speedup on two pinned physical cores comes out at −.
| rule | value | chosen |
|---|
That is every threshold in the reduction, − of them, and − were chosen after seeing measurements: −. Each says so in its own row, and the count in this sentence is counted from the table rather than typed into it. The marginal floor has its entire safe window published on the slider in section 03. The impossible serial fraction bound sits at 1.10 rather than at the principled 1.0 because the honest degenerate case, a single hardware thread, legitimately measures a fraction just over 1; the gap it sits in is measured, with the honest cases below − and the duplicating control at −. The busy floor sits at 1.5 in the same way, between the worst honest capture at − and the deliberately loaded one at −, and that is a narrower gap than the serial fraction bound gets.
The first four rounds of captures are not in the repository, and they were all wrong in the same way: the job size was chosen from a single timing probe taken before the runtime had settled, so the sweep at one worker count ran at one compiled speed and the sweep at another ran at a different one. The visible symptom was a speedup below 1 at two workers on a machine with four hardware threads, which is not a subtle error and did not look like one. What makes it worth recording is that every check on the page would have passed: the arithmetic was right, the checksums added up, the reduction agreed with itself, the offline mirror agreed with the browser. Only the refusal rules noticed, and only because a serial fraction above 1 is impossible rather than merely surprising. The controls in section 05 are there because of that morning.
Every quotation on this page is pulled out of a file committed under research/how-many-cores-do-you-have/data/sources/ by 06-sources.mjs, which fails rather than let the page paraphrase. −
| work | record, and whether the full text was obtained |
|---|
Two of those three could not be read. Gustafson 1988 and Karp and Flatt 1990 are both behind the ACM Digital Library, which returns 403 to this machine, and no primary copy was found. So nothing is quoted from either, none of the figures those papers are famous for appear anywhere on this page, and the arithmetic attributed to them in sections 04 and 07 is derived here from Amdahl’s expression and from the definition of weak scaling rather than copied. Their bibliographic records were confirmed field by field against the publisher’s own Crossref metadata for each DOI, which is a check on the citation and not on the contents.
The engines. Fetched from the three projects’ own repositories on the date recorded in the table above. Browser engines change; the page quotes the committed copies and both verifiers check the quotations against those copies, not against the network, so this page cannot start misquoting somebody because a file moved.
sh research/how-many-cores-do-you-have/00-fetch-sources.sh
node research/how-many-cores-do-you-have/00-topology.mjs
sh research/how-many-cores-do-you-have/run-ground-truth.sh
sh research/how-many-cores-do-you-have/run-controls.sh
node research/how-many-cores-do-you-have/03-engine-numbers.mjs
node research/how-many-cores-do-you-have/04-calibration.mjs
node research/how-many-cores-do-you-have/05-weak-scaling.mjs
node research/how-many-cores-do-you-have/06-sources.mjs
node research/how-many-cores-do-you-have/07-jit-tiering.mjs
node research/how-many-cores-do-you-have/08-build-page-data.mjs
node research/how-many-cores-do-you-have/09-verify.mjs
node verify-how-many-cores-do-you-have.mjs
Everything from 06 onward is offline and reproduces every number on this page from the committed files. The capture steps will produce your traces on your topology, not these ones, and the pinning in run-ground-truth.sh assumes the capture machine’s core layout, which is printed by 00-topology.mjs.