Skip to content

compiler: rework comptime pointer representation and access - #19630

Merged
andrewrk merged 5 commits into
ziglang:masterfrom
mlugg:comptime-ptr-access-5
Apr 17, 2024
Merged

compiler: rework comptime pointer representation and access#19630
andrewrk merged 5 commits into
ziglang:masterfrom
mlugg:comptime-ptr-access-5

Conversation

@mlugg

Copy link
Copy Markdown
Member

We've got a big one here! This commit reworks how we represent pointers in the InternPool, and rewrites the logic for loading and storing from them at comptime.

Firstly, the pointer representation. Previously, pointers were represented in a highly structured manner: pointers to fields, array elements, etc, were explicitly represented. This works well for simple cases, but is quite difficult to handle in the cases of unusual reinterpretations, pointer casts, offsets, etc. Therefore, pointers are now represented in a more "flat" manner. For types without well-defined layouts -- such as comptime-only types, automatic-layout aggregates, and so on -- we still use this "hierarchical" structure. However, for types with well-defined layouts, we use a byte offset associated with the pointer. This allows the comptime pointer access logic to deal with reinterpreted pointers far more gracefully, because the "base address" of a pointer -- for instance a field -- is a single value which pointer accesses cannot exceed since the parent has undefined layout. This strategy is also more useful to most backends -- see the updated logic in codegen.zig and codegen/llvm.zig. For backends which do prefer a chain of field and elements accesses for lowering pointer values, such as SPIR-V, there is a helpful function in Value which creates a strategy to derive a pointer value using ideally only field and element accesses. This is actually more correct than the previous logic, since it correctly handles pointer casts which, after the dust has settled, end up referring exactly to an aggregate field or array element.

In terms of the pointer access code, it has been rewritten from the ground up. The old logic had become rather a mess of special cases being added whenever bugs were hit, and was still riddled with bugs. The new logic was written to handle the "difficult" cases correctly, the most notable of which is restructuring of a comptime-only array (for instance, converting a [3][2]comptime_int to a [2][3]comptime_int. Currently, the logic for loading and storing work somewhat differently, but a future change will likely improve the loading logic to bring it more in line with the store strategy. As far as I can tell, the rewrite has fixed all bugs exposed by #19414.

As a part of this, the comptime bitcast logic has also been rewritten. Previously, bitcasts simply worked by serializing the entire value into an in-memory buffer, then deserializing it. This strategy has two key weaknesses: pointers, and undefined values. Representations of these values at comptime cannot be easily serialized/deserialized whilst preserving data, which means many bitcasts would become runtime-known if pointers were involved, or would turn undefined values into 0xAA. The new logic works by "flattening" the datastructure to be cast into a sequence of bit-packed atomic values, and then "unflattening" it; using serialization when necessary, but with special handling for undefined values and for pointers which align in virtual memory. The resulting code is definitely slower -- more on this later -- but it is correct.

The pointer access and bitcast logic required some helper functions and types which are not generally useful elsewhere, so I opted to split them into separate files Sema/comptime_ptr_access.zig and Sema/bitcast.zig, with simple re-exports in Sema.zig for their small public APIs.

Whilst working on this branch, I caught various unrelated bugs with transitive Sema errors, and with the handling of undefined values. These bugs have been fixed, and corresponding behavior test added.

In terms of performance, I do anticipate that this commit will regress performance somewhat, because the new pointer access and bitcast logic is necessarily more complex. I have not yet taken performance measurements, but will do shortly, and post the results in this PR. If the performance regression is severe, I will do work to to optimize the new logic before merge.

Resolves: #19452
Resolves: #19460

@mlugg
mlugg requested a review from Snektron as a code ownerApril 12, 2024 09:25
@matklad

Copy link
Copy Markdown
Contributor

These descriptions are always a joy to read if you are following along, thanks mlugg!

@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch 4 times, most recently from f15a275 to 83e8d6dCompareApril 12, 2024 19:30
@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch 4 times, most recently from 4657609 to 1e2df59CompareApril 13, 2024 00:56
@mlugg
mlugg requested a review from squeek502 as a code ownerApril 13, 2024 00:56
Comment threadsrc/codegen/spirv.zig
Comment threadsrc/codegen/spirv.zig Outdated
@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch 4 times, most recently from 40f4999 to 9a0131eCompareApril 14, 2024 17:55
@mlugg

Copy link
Copy Markdown
MemberAuthor

Okay, after correctly handling endianness, the new bitcast logic has become a bit messy. This is probably due some improvement, but since this PR is blocking 0.12 and actively blocking some major projects, I don't think that should be a PR blocker.

@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch 3 times, most recently from 332ec8c to 6c55960CompareApril 15, 2024 16:31
@mlugg

mlugg commented Apr 16, 2024

Copy link
Copy Markdown
MemberAuthor

A pleasant surprise: I'm seeing zero significant performance deltas between master and this branch! I guess the comptime pointer access code just isn't hit often enough to make any difference, or perhaps the perf delta is just less than I thought.

Build Behavior (x86_64)

Note: this was tested on a common subset of behavior tests which pass on both master and this branch.

Benchmark 1 (25 runs): /home/mlugg/zig/master/build/stage3/bin/zig test /home/mlugg/zig/master/test/behavior.zig -fno-llvm -fno-lld --test-no-exec
measurement mean ± σ min … max outliers delta
wall_time 804ms ± 17.3ms 792ms … 846ms 5 (20%) 0%
peak_rss 114MB ± 1.49MB 114MB … 121MB 1 ( 4%) 0%
cpu_cycles 3.31G ± 91.4M 3.26G … 3.68G 4 (16%) 0%
instructions 6.08G ± 77.8M 6.07G … 6.46G 2 ( 8%) 0%
cache_references 248M ± 5.45M 232M … 253M 5 (20%) 0%
cache_misses 33.3M ± 477K 32.3M … 34.2M 0 ( 0%) 0%
branch_misses 25.0M ± 342K 24.8M … 26.6M 2 ( 8%) 0%
Benchmark 2 (26 runs): /home/mlugg/zig/comptime-ptr-access-5/stage4/bin/zig test /home/mlugg/zig/master/test/behavior.zig -fno-llvm -fno-lld --test-no-exec
measurement mean ± σ min … max outliers delta
wall_time 798ms ± 3.36ms 793ms … 808ms 0 ( 0%) - 0.8% ± 0.9%
peak_rss 116MB ± 1.50MB 116MB … 124MB 1 ( 4%) 💩+ 1.8% ± 0.7%
cpu_cycles 3.28G ± 52.3M 3.25G … 3.52G 1 ( 4%) - 0.9% ± 1.3%
instructions 6.15G ± 76.4M 6.13G … 6.52G 1 ( 4%) + 1.0% ± 0.7%
cache_references 250M ± 2.55M 248M … 261M 2 ( 8%) + 0.8% ± 1.0%
cache_misses 33.1M ± 253K 32.7M … 33.7M 0 ( 0%) - 0.5% ± 0.6%
branch_misses 24.9M ± 319K 24.7M … 26.4M 1 ( 4%) - 0.6% ± 0.7%

Analyze Compiler

Benchmark 1 (6 runs): <snip>
measurement mean ± σ min … max outliers delta
wall_time 5.17s ± 249ms 5.01s … 5.63s 0 ( 0%) 0%
peak_rss 220MB ± 176KB 220MB … 220MB 0 ( 0%) 0%
cpu_cycles 23.5G ± 1.18G 22.7G … 25.7G 0 ( 0%) 0%
instructions 41.5G ± 23.2K 41.5G … 41.5G 0 ( 0%) 0%
cache_references 2.06G ± 28.4M 2.02G … 2.08G 0 ( 0%) 0%
cache_misses 83.3M ± 1.67M 80.3M … 85.3M 0 ( 0%) 0%
branch_misses 70.5M ± 619K 69.9M … 71.6M 0 ( 0%) 0%
Benchmark 2 (6 runs): <snip>
measurement mean ± σ min … max outliers delta
wall_time 5.10s ± 102ms 5.03s … 5.27s 0 ( 0%) - 1.4% ± 4.7%
peak_rss 234MB ± 171KB 234MB … 235MB 0 ( 0%) 💩+ 6.6% ± 0.1%
cpu_cycles 23.3G ± 458M 23.0G … 24.0G 0 ( 0%) - 0.8% ± 4.9%
instructions 42.7G ± 21.3K 42.7G … 42.7G 0 ( 0%) 💩+ 2.8% ± 0.0%
cache_references 2.12G ± 18.9M 2.11G … 2.16G 0 ( 0%) 💩+ 3.1% ± 1.5%
cache_misses 90.1M ± 3.33M 87.4M … 96.4M 0 ( 0%) 💩+ 8.1% ± 4.1%
branch_misses 71.7M ± 928K 71.1M … 73.6M 0 ( 0%) + 1.7% ± 1.4%

Build Compiler (x86_64)

Benchmark 1 (3 runs): <snip>
measurement mean ± σ min … max outliers delta
wall_time 15.1s ± 74.9ms 15.0s … 15.2s 0 ( 0%) 0%
peak_rss 356MB ± 234KB 356MB … 357MB 0 ( 0%) 0%
cpu_cycles 65.1G ± 795M 64.5G … 66.0G 0 ( 0%) 0%
instructions 130G ± 287K 130G … 130G 0 ( 0%) 0%
cache_references 4.08G ± 45.2M 4.05G … 4.13G 0 ( 0%) 0%
cache_misses 498M ± 2.43M 495M … 500M 0 ( 0%) 0%
branch_misses 449M ± 590K 449M … 450M 0 ( 0%) 0%
Benchmark 2 (3 runs): <snip>
measurement mean ± σ min … max outliers delta
wall_time 15.1s ± 165ms 14.9s … 15.2s 0 ( 0%) - 0.1% ± 1.9%
peak_rss 363MB ± 67.4KB 363MB … 364MB 0 ( 0%) 💩+ 2.0% ± 0.1%
cpu_cycles 64.8G ± 42.1M 64.8G … 64.8G 0 ( 0%) - 0.4% ± 2.0%
instructions 131G ± 196K 131G … 131G 0 ( 0%) + 0.9% ± 0.0%
cache_references 4.19G ± 3.56M 4.19G … 4.20G 0 ( 0%) + 2.7% ± 1.8%
cache_misses 500M ± 2.19M 498M … 502M 0 ( 0%) + 0.4% ± 1.1%
branch_misses 446M ± 1.14M 445M … 447M 0 ( 0%) - 0.7% ± 0.5%

@andrewrkandrewrk left a comment

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 implements a non-existing, non-accepted, breaking proposal to change the language semantics regarding loading values with partially undefined bits.

It is not clear whether these semantics are desired. A proposal that introduces a new kind of illegal behavior must address the question of how it will be safety-checked, if at all. If the proposal cannot provide a strategy for safety checking, it will probably be rejected, in which case this PR would introduce just as many regressions as it fixes.

I don't think this should be part of 0.12.0.

Comment threadlib/std/net.zig Outdated
Comment threadlib/std/packed_int_array.zig Outdated
@mlugg

Copy link
Copy Markdown
MemberAuthor

The commit I just pushed effectively un-implements #19634, and removes the workarounds in std. For now, any undefined bits in a partially-defined value revert to status quo behavior, which is to drop the comptime-known undefined-ness and turn the bits into 0xAA.

This decision can be revisited down the line: to either accept that proposal with some runtime safety, or to introduce tracking of partially-undefined values at comptime.

@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch 2 times, most recently from 5a4026e to 1719adaCompareApril 17, 2024 10:43
mlugg added 4 commits April 17, 2024 13:41
We've got a big one here! This commit reworks how we represent pointers
in the InternPool, and rewrites the logic for loading and storing from
them at comptime.
Firstly, the pointer representation. Previously, pointers were
represented in a highly structured manner: pointers to fields, array
elements, etc, were explicitly represented. This works well for simple
cases, but is quite difficult to handle in the cases of unusual
reinterpretations, pointer casts, offsets, etc. Therefore, pointers are
now represented in a more "flat" manner. For types without well-defined
layouts -- such as comptime-only types, automatic-layout aggregates, and
so on -- we still use this "hierarchical" structure. However, for types
with well-defined layouts, we use a byte offset associated with the
pointer. This allows the comptime pointer access logic to deal with
reinterpreted pointers far more gracefully, because the "base address"
of a pointer -- for instance a `field` -- is a single value which
pointer accesses cannot exceed since the parent has undefined layout.
This strategy is also more useful to most backends -- see the updated
logic in `codegen.zig` and `codegen/llvm.zig`. For backends which do
prefer a chain of field and elements accesses for lowering pointer
values, such as SPIR-V, there is a helpful function in `Value` which
creates a strategy to derive a pointer value using ideally only field
and element accesses. This is actually more correct than the previous
logic, since it correctly handles pointer casts which, after the dust
has settled, end up referring exactly to an aggregate field or array
element.
In terms of the pointer access code, it has been rewritten from the
ground up. The old logic had become rather a mess of special cases being
added whenever bugs were hit, and was still riddled with bugs. The new
logic was written to handle the "difficult" cases correctly, the most
notable of which is restructuring of a comptime-only array (for
instance, converting a `[3][2]comptime_int` to a `[2][3]comptime_int`.
Currently, the logic for loading and storing work somewhat differently,
but a future change will likely improve the loading logic to bring it
more in line with the store strategy. As far as I can tell, the rewrite
has fixed all bugs exposed by ziglang#19414.
As a part of this, the comptime bitcast logic has also been rewritten.
Previously, bitcasts simply worked by serializing the entire value into
an in-memory buffer, then deserializing it. This strategy has two key
weaknesses: pointers, and undefined values. Representations of these
values at comptime cannot be easily serialized/deserialized whilst
preserving data, which means many bitcasts would become runtime-known if
pointers were involved, or would turn `undefined` values into `0xAA`.
The new logic works by "flattening" the datastructure to be cast into a
sequence of bit-packed atomic values, and then "unflattening" it; using
serialization when necessary, but with special handling for `undefined`
values and for pointers which align in virtual memory. The resulting
code is definitely slower -- more on this later -- but it is correct.
The pointer access and bitcast logic required some helper functions and
types which are not generally useful elsewhere, so I opted to split them
into separate files `Sema/comptime_ptr_access.zig` and
`Sema/bitcast.zig`, with simple re-exports in `Sema.zig` for their small
public APIs.
Whilst working on this branch, I caught various unrelated bugs with
transitive Sema errors, and with the handling of `undefined` values.
These bugs have been fixed, and corresponding behavior test added.
In terms of performance, I do anticipate that this commit will regress
performance somewhat, because the new pointer access and bitcast logic
is necessarily more complex. I have not yet taken performance
measurements, but will do shortly, and post the results in this PR. If
the performance regression is severe, I will do work to to optimize the
new logic before merge.
Resolves: ziglang#19452Resolves: ziglang#19460
I have noted several latent bugs in the handling of packed memory on
big-endian targets, and one of them has been exposed by the previous
commit, triggering this test failure. I am opting to disable it for now
on the ground that the commit preceding this one helps far more than it
hurts.
This commit reverts the handling of partially-undefined values in
bitcasting to transform these bits into an arbitrary numeric value,
like happens on `master` today.
As @andrewrk rightly points out, ziglang#19634 has unfortunate consequences
for the standard library, and likely requires more thought. To avoid
a major breaking change, it has been decided to revert this design
decision for now, and make a more informed decision further down the
line.
I have no idea why this wasn't being hit on master before.
The operation `undefined & 0` ought to result in the value `0`, and likewise for
zeroing only some bits. `std/packed_int_array.zig` tests were failing because
this behavior was not implemented -- this issue was previously masked by faulty
bitcast logic which turned `undefined` values into `0xAA` on pointer loads.
Ideally, we would like to be able to track the undefined bits at comptime.
This is related to ziglang#19634.
@mlugg
mluggforce-pushed the comptime-ptr-access-5 branch from 1719ada to 23062a5CompareApril 17, 2024 12:41
@andrewrk

Copy link
Copy Markdown
Member

Brilliant work. Thanks for that follow-up.

Sign up for freeto 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.

"error: Global variable contains reference to comptime var" when it shouldn't Comptime dereference bugs tracking issue

4 participants

@mlugg@matklad@andrewrk@Snektron