authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-05 19:00:38+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-10 10:26:15+00:00
logc73db56b4543776ce157626361f1cb66df1dac69
tree38c5e085f6c3b78b21b627ed18204e760ef3e4fe
parentea7e34224a8c3c0551e17a60fd849efee82447f7
signature Commit is signed but in an unrecognized format.

compiler: small optimizations

With these optimizations in place, this branch is faster than master in ReleaseFast, and... not too much slower than master in ReleaseSafe.

2 files changed, 117 insertions(+), 56 deletions(-)

src/Sema.zig+4-2
...@@ -2287,13 +2287,15 @@ fn resolveValue(sema: *Sema, inst: Air.Inst.Ref) ?Value {...@@ -2287,13 +2287,15 @@ fn resolveValue(sema: *Sema, inst: Air.Inst.Ref) ?Value {
2287 .inferred_alloc_comptime => unreachable, // assertion failure2287 .inferred_alloc_comptime => unreachable, // assertion failure
2288 else => {},2288 else => {},
2289 }2289 }
2290 switch (sema.typeOf(inst).classify(zcu)) {2290 // LLVM fails to eliminate this `classify` call in ReleaseFast, which hurts performance, so
2291 // we must explicitly check for `std.debug.runtime_safety`.
2292 if (std.debug.runtime_safety) switch (sema.typeOf(inst).classify(zcu)) {
2291 .no_possible_value => unreachable, // values of this type do not exist2293 .no_possible_value => unreachable, // values of this type do not exist
2292 .one_possible_value => unreachable, // the value should be comptime-known2294 .one_possible_value => unreachable, // the value should be comptime-known
2293 .partially_comptime => unreachable, // the value should be comptime-known2295 .partially_comptime => unreachable, // the value should be comptime-known
2294 .fully_comptime => unreachable, // the value should be comptime-known2296 .fully_comptime => unreachable, // the value should be comptime-known
2295 .runtime => {},2297 .runtime => {},
2296 }2298 };
2297 return null;2299 return null;
2298 }2300 }
2299}2301}
src/Type.zig+113-54
...@@ -112,10 +112,17 @@ pub const Class = enum(u3) {...@@ -112,10 +112,17 @@ pub const Class = enum(u3) {
112};112};
113113
114/// Returns the `Class` for the type `ty`. Asserts that the layout of `ty` is resolved.114/// Returns the `Class` for the type `ty`. Asserts that the layout of `ty` is resolved.
115pub fn classify(ty: Type, zcu: *const Zcu) Class {115pub fn classify(start_ty: Type, zcu: *const Zcu) Class {
116 ty.assertHasLayout(zcu);
117 const ip = &zcu.intern_pool;116 const ip = &zcu.intern_pool;
118 return switch (ip.indexToKey(ty.toIntern())) {117
118 // We avoid recursion in most cases to make us more optimizer-friendly because this can be a
119 // very hot code path. The only case where recursion is necessary is tuples, so that case is
120 // outlined into a separate function; see `classifyTuple`.
121
122 var extra_states: enum { none, one, many } = .none;
123
124 var cur_ty = start_ty;
125 const base: Class = while (true) break switch (ip.indexToKey(cur_ty.toIntern())) {
119 .simple_type => |t| switch (t) {126 .simple_type => |t| switch (t) {
120 .f16,127 .f16,
121 .f32,128 .f32,
...@@ -165,17 +172,10 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {...@@ -165,17 +172,10 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
165172
166 .opaque_type => .no_possible_value,173 .opaque_type => .no_possible_value,
167174
168 .error_union_type => |eu| switch (Type.fromInterned(eu.payload_type).classify(zcu)) {175 .error_union_type => |eu| {
169 .no_possible_value,176 extra_states = .many;
170 .one_possible_value,177 cur_ty = .fromInterned(eu.payload_type);
171 .runtime,178 continue;
172 => .runtime,
173
174 .partially_comptime => .partially_comptime,
175 // It may seem that this should be `.partially_comptime` due to the error set, however
176 // there is no way to take a pointer to the error set of an error union, so it does not
177 // actually necessitate runtime bits.
178 .fully_comptime => .fully_comptime,
179 },179 },
180180
181 .int_type => |int| switch (int.bits) {181 .int_type => |int| switch (int.bits) {
...@@ -183,55 +183,58 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {...@@ -183,55 +183,58 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
183 else => .runtime,183 else => .runtime,
184 },184 },
185 .array_type => |arr| {185 .array_type => |arr| {
186 if (arr.len == 0 and arr.sentinel == .none) return .one_possible_value;186 if (arr.len == 0 and arr.sentinel == .none) break .one_possible_value;
187 return Type.fromInterned(arr.child).classify(zcu);187 cur_ty = .fromInterned(arr.child);
188 continue;
188 },189 },
189 .vector_type => |vec| {190 .vector_type => |vec| {
190 if (vec.len == 0) return .one_possible_value;191 if (vec.len == 0) break .one_possible_value;
191 return Type.fromInterned(vec.child).classify(zcu);192 cur_ty = .fromInterned(vec.child);
193 continue;
192 },194 },
193 .opt_type => |child| switch (Type.fromInterned(child).classify(zcu)) {195 .opt_type => |child_ty_ip| {
194 .no_possible_value => .one_possible_value,196 extra_states = switch (extra_states) {
195 .one_possible_value => .runtime,197 .none => .one,
196 else => |class| class,198 .one, .many => .many,
199 };
200 cur_ty = .fromInterned(child_ty_ip);
201 continue;
197 },202 },
198 .tuple_type => |tuple| {203 .tuple_type => |tuple| {
199 var has_runtime_state = false;204 @branchHint(.unlikely);
200 var has_comptime_state = false;205 break classifyTuple(tuple.types.get(ip), tuple.values.get(ip), zcu);
201 for (tuple.types.get(ip), tuple.values.get(ip)) |field_ty, field_comptime_val| {
202 if (field_comptime_val != .none) continue;
203 switch (Type.fromInterned(field_ty).classify(zcu)) {
204 .no_possible_value => return .no_possible_value,
205 .one_possible_value => {},
206 .runtime => has_runtime_state = true,
207 .fully_comptime => has_comptime_state = true,
208 .partially_comptime => {
209 has_runtime_state = true;
210 has_comptime_state = true;
211 },
212 }
213 }
214 if (has_comptime_state) {
215 return if (has_runtime_state) .partially_comptime else .fully_comptime;
216 } else {
217 return if (has_runtime_state) .runtime else .one_possible_value;
218 }
219 },206 },
220 .struct_type => {207 .struct_type => {
221 const struct_obj = ip.loadStructType(ty.toIntern());208 const struct_obj = ip.loadStructType(cur_ty.toIntern());
222 return switch (struct_obj.layout) {209 switch (struct_obj.layout) {
223 .auto, .@"extern" => struct_obj.class,210 .auto, .@"extern" => {
224 .@"packed" => Type.fromInterned(struct_obj.packed_backing_int_type).classify(zcu),211 zcu.assertUpToDate(.wrap(.{ .type_layout = cur_ty.toIntern() }));
225 };212 break struct_obj.class;
213 },
214 .@"packed" => {
215 cur_ty = .fromInterned(struct_obj.packed_backing_int_type);
216 continue;
217 },
218 }
226 },219 },
227 .union_type => {220 .union_type => {
228 const union_obj = ip.loadUnionType(ty.toIntern());221 const union_obj = ip.loadUnionType(cur_ty.toIntern());
229 return switch (union_obj.layout) {222 switch (union_obj.layout) {
230 .auto, .@"extern" => union_obj.class,223 .auto, .@"extern" => {
231 .@"packed" => Type.fromInterned(union_obj.packed_backing_int_type).classify(zcu),224 zcu.assertUpToDate(.wrap(.{ .type_layout = cur_ty.toIntern() }));
232 };225 break union_obj.class;
226 },
227 .@"packed" => {
228 cur_ty = .fromInterned(union_obj.packed_backing_int_type);
229 continue;
230 },
231 }
232 },
233 .enum_type => {
234 zcu.assertUpToDate(.wrap(.{ .type_layout = cur_ty.toIntern() }));
235 cur_ty = .fromInterned(ip.loadEnumType(cur_ty.toIntern()).int_tag_type);
236 continue;
233 },237 },
234 .enum_type => Type.fromInterned(ip.loadEnumType(ty.toIntern()).int_tag_type).classify(zcu),
235238
236 // values, not types239 // values, not types
237 .undef,240 .undef,
...@@ -255,6 +258,53 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {...@@ -255,6 +258,53 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
255 .memoized_call,258 .memoized_call,
256 => unreachable,259 => unreachable,
257 };260 };
261
262 return switch (base) {
263 .runtime => .runtime, // extra states are irrelevant, we already have many!
264 .partially_comptime => .partially_comptime, // likewise
265 .fully_comptime => {
266 // We do not need to change to `.partially_comptime` here because the extra states do
267 // not necessarily require runtime bits. This is because Zig does not provide a way to
268 // take the address of the "is null" bit of an optional or the error set "inside" of an
269 // error union.
270 return .fully_comptime;
271 },
272
273 .no_possible_value => switch (extra_states) {
274 .none => .no_possible_value,
275 .one => .one_possible_value,
276 .many => .runtime,
277 },
278
279 .one_possible_value => switch (extra_states) {
280 .none => .one_possible_value,
281 .one, .many => .runtime,
282 },
283 };
284}
285/// This is a separate function to `classify` to avoid recursion in the main `classify` function,
286/// which can encourage the optimizer to e.g. inline `classify` where it would be beneficial.
287fn classifyTuple(types: []const InternPool.Index, values: []const InternPool.Index, zcu: *const Zcu) Class {
288 var has_runtime_state = false;
289 var has_comptime_state = false;
290 for (types, values) |field_ty, field_comptime_val| {
291 if (field_comptime_val != .none) continue;
292 switch (Type.fromInterned(field_ty).classify(zcu)) {
293 .no_possible_value => return .no_possible_value,
294 .one_possible_value => {},
295 .runtime => has_runtime_state = true,
296 .fully_comptime => has_comptime_state = true,
297 .partially_comptime => {
298 has_runtime_state = true;
299 has_comptime_state = true;
300 },
301 }
302 }
303 if (has_comptime_state) {
304 return if (has_runtime_state) .partially_comptime else .fully_comptime;
305 } else {
306 return if (has_runtime_state) .runtime else .one_possible_value;
307 }
258}308}
259309
260/// Asserts the type is resolved.310/// Asserts the type is resolved.
...@@ -1061,7 +1111,10 @@ pub fn abiSize(ty: Type, zcu: *const Zcu) u64 {...@@ -1061,7 +1111,10 @@ pub fn abiSize(ty: Type, zcu: *const Zcu) u64 {
1061 .error_union_type => |error_union| {1111 .error_union_type => |error_union| {
1062 const payload_ty: Type = .fromInterned(error_union.payload_type);1112 const payload_ty: Type = .fromInterned(error_union.payload_type);
1063 switch (payload_ty.classify(zcu)) {1113 switch (payload_ty.classify(zcu)) {
1064 .fully_comptime => return 0, // error set does not require runtime bits, see comment in `classify`1114 // Zig has no way to take the address of the error set "in" an error union (giving
1115 // implementations more freedom in terms of data layout), so if the payload type is
1116 // fully comptime, we don't need to dedicate runtime bits to the error set.
1117 .fully_comptime => return 0,
1065 else => {},1118 else => {},
1066 }1119 }
1067 // The layout will either be (code, payload, padding) or (payload, code, padding)1120 // The layout will either be (code, payload, padding) or (payload, code, padding)
...@@ -3177,6 +3230,12 @@ fn validateExternCallconv(cc: std.builtin.CallingConvention) bool {...@@ -3177,6 +3230,12 @@ fn validateExternCallconv(cc: std.builtin.CallingConvention) bool {
31773230
3178/// Asserts that `ty` has resolved layout.3231/// Asserts that `ty` has resolved layout.
3179pub fn assertHasLayout(ty: Type, zcu: *const Zcu) void {3232pub fn assertHasLayout(ty: Type, zcu: *const Zcu) void {
3233 if (!std.debug.runtime_safety) {
3234 // This early exit isn't necessary (`Zcu.assertUpToDate` checks `std.debug.runtime_safety`
3235 // itself), but LLVM has been observed to fail at optimizing away this safety check, which
3236 // has a major performance impact on ReleaseFast compiler builds.
3237 return;
3238 }
3180 switch (zcu.intern_pool.indexToKey(ty.toIntern())) {3239 switch (zcu.intern_pool.indexToKey(ty.toIntern())) {
3181 .int_type,3240 .int_type,
3182 .ptr_type,3241 .ptr_type,