Skip to content

Pack Task.gostate{,Seq,Time} into a single 64-bit atomic int. - #15005

Open
EtiennePerot wants to merge 1 commit into
google:masterfrom
EtiennePerot:gostate-packing
Open

EtiennePerot wants to merge 1 commit into
google:masterfrom
EtiennePerot:gostate-packing

Conversation

@EtiennePerot

@EtiennePerot EtiennePerot commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Task used to have two fields (gostate and gostateTime) which are updated very frequently in lockstep (since the latter is the modification time of the former). To do so lock-less-ly, it used a sync.SeqCount to try to update them atomically.

However, in practice, gostateTime (number of nanoseconds since boot) never really need the full 64 bits, because sandboxes don't run for multiple centuries. So we can pack the gostate enum bits together with the timestamp bits into a single 64-bit field which can be written with native atomic operations, which is already much faster.

The only downside is that now, sandboxes can only run for up to 2^61 - 1 nanoseconds, aka around 73 years, whereas before they could run for 2^63 - 1 nanoseconds, aka around 292 years. So we've lost the ability to run for a century.

But wait, there's more. All the readers of this field tolerate stale reads. This means we can actually not use an atomic write at all, so long as we can guarantee that all 64 bits either get written or don't (i.e. no partial writes). This is the case for 64-bit architectures, but not 32-bit. So this introduces a new StoreRelaxed method on atomicbitops.Uint64, which is just a plain a = b for 64-bit architectures, but otherwise a regular 64-bit atomic store for 32-bit architectures where this second optimization doesn't apply.

Benchmarks:

                                                 │   before    │                 after                 │
                                                 │   sec/op    │   sec/op     vs base                  │
laptop/platform=kvm/bench=getpid                   410.0n ± 1%   380.0n ± 1%  -7.32% (p=0.002 n=6)
laptop/platform=kvm/bench=getpidopt                407.5n ± 2%   376.5n ± 1%  -7.61% (p=0.002 n=6)
laptop/platform=systrap/bench=getpid               2.200µ ± 0%   2.177µ ± 1%  -1.05% (p=0.001 n=15)
laptop/platform=systrap/bench=getpidopt            709.8n ± 2%   682.6n ± 1%  -3.83% (p=0.000 n=14+15)
n2-standard-16/platform=kvm/bench=getpid           579.4n ± 1%   537.7n ± 1%  -7.19% (p=0.000 n=32)
n2-standard-16/platform=kvm/bench=getpidopt        579.3n ± 0%   533.4n ± 0%  -7.92% (p=0.000 n=32)
n2-standard-16/platform=systrap/bench=getpid       3.669µ ± 1%   3.632µ ± 1%  -1.01% (p=0.000 n=32+33)
n2-standard-16/platform=systrap/bench=getpidopt    1.211µ ± 1%   1.182µ ± 1%  -2.39% (p=0.000 n=32+33)

`Task` used to have two fields (`gostate` and `gostateTime`) which are
updated very frequently in lockstep (since the latter is the modification
time of the former). To do so lock-less-ly, it used a `sync.SeqCount` to
try to update them atomically.

However, in practice, `gostateTime` (number of nanoseconds since boot) never
reaches 64 bits, because sandboxes don't run for multiple centuries. So we
can pack the `gostate` enum bits together with the timestamp bits into a
single 64-bit field which can be written with native atomic operations,
which is already much faster.

The only downside is that now, sandboxes can only run for up to
`2^61 - 1` nanoseconds, aka around 73 years, whereas before they could run
for `2^63 - 1` nanoseconds, aka around 292 years. So we've lost the ability
to run for a century.

But wait, there's more. All the readers of this field tolerate stale reads.
This means we can actually not use an atomic write at all, so long as we
can guarantee that all 64 bits either get written or don't (i.e. no partial
writes). This is the case for 64-bit architectures, but not 32-bit.
So this introduces a new `StoreRelaxed` method on `atomicbitops.Uint64`,
which is just a plain `a = b` for 64-bit architectures, but otherwise a
regular atomic store for 32-bit architectures where this second optimization
doesn't apply.

Benchmarks:

```
                                                 │   before    │                 after                 │
                                                 │   sec/op    │   sec/op     vs base                  │
laptop/platform=kvm/bench=getpid                   410.0n ± 1%   380.0n ± 1%  -7.32% (p=0.002 n=6)
laptop/platform=kvm/bench=getpidopt                407.5n ± 2%   376.5n ± 1%  -7.61% (p=0.002 n=6)
laptop/platform=systrap/bench=getpid               2.200µ ± 0%   2.177µ ± 1%  -1.05% (p=0.001 n=15)
laptop/platform=systrap/bench=getpidopt            709.8n ± 2%   682.6n ± 1%  -3.83% (p=0.000 n=14+15)
n2-standard-16/platform=kvm/bench=getpid           579.4n ± 1%   537.7n ± 1%  -7.19% (p=0.000 n=32)
n2-standard-16/platform=kvm/bench=getpidopt        579.3n ± 0%   533.4n ± 0%  -7.92% (p=0.000 n=32)
n2-standard-16/platform=systrap/bench=getpid       3.669µ ± 1%   3.632µ ± 1%  -1.01% (p=0.000 n=32+33)
n2-standard-16/platform=systrap/bench=getpidopt    1.211µ ± 1%   1.182µ ± 1%  -2.39% (p=0.000 n=32+33)
```
@ayushr2

ayushr2 commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

Once again, very cool! Do you know why this improves KVM more than systrap?

@EtiennePerot

EtiennePerot commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Once again, very cool! Do you know why this improves KVM more than systrap?

@ayushr2 It is a flat improvement for both, around 30~40ns. Since KVM syscall roundtrip time is smaller, that flat improvement translates to a larger proportion of its roundtrip time, so on a percentage basis it looks bigger. But it's the same in absolute terms.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants