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
signaturelock-open 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 {
22872287 .inferred_alloc_comptime => unreachable, // assertion failure
22882288 else => {},
22892289 }
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)) {
22912293 .no_possible_value => unreachable, // values of this type do not exist
22922294 .one_possible_value => unreachable, // the value should be comptime-known
22932295 .partially_comptime => unreachable, // the value should be comptime-known
22942296 .fully_comptime => unreachable, // the value should be comptime-known
22952297 .runtime => {},
2296 }
2298 };
22972299 return null;
22982300 }
22992301}
src/Type.zig+113-54
......@@ -112,10 +112,17 @@ pub const Class = enum(u3) {
112112};
113113
114114/// Returns the `Class` for the type `ty`. Asserts that the layout of `ty` is resolved.
115pub fn classify(ty: Type, zcu: *const Zcu) Class {
116 ty.assertHasLayout(zcu);
115pub fn classify(start_ty: Type, zcu: *const Zcu) Class {
117116 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())) {
119126 .simple_type => |t| switch (t) {
120127 .f16,
121128 .f32,
......@@ -165,17 +172,10 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
165172
166173 .opaque_type => .no_possible_value,
167174
168 .error_union_type => |eu| switch (Type.fromInterned(eu.payload_type).classify(zcu)) {
169 .no_possible_value,
170 .one_possible_value,
171 .runtime,
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,
175 .error_union_type => |eu| {
176 extra_states = .many;
177 cur_ty = .fromInterned(eu.payload_type);
178 continue;
179179 },
180180
181181 .int_type => |int| switch (int.bits) {
......@@ -183,55 +183,58 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
183183 else => .runtime,
184184 },
185185 .array_type => |arr| {
186 if (arr.len == 0 and arr.sentinel == .none) return .one_possible_value;
187 return Type.fromInterned(arr.child).classify(zcu);
186 if (arr.len == 0 and arr.sentinel == .none) break .one_possible_value;
187 cur_ty = .fromInterned(arr.child);
188 continue;
188189 },
189190 .vector_type => |vec| {
190 if (vec.len == 0) return .one_possible_value;
191 return Type.fromInterned(vec.child).classify(zcu);
191 if (vec.len == 0) break .one_possible_value;
192 cur_ty = .fromInterned(vec.child);
193 continue;
192194 },
193 .opt_type => |child| switch (Type.fromInterned(child).classify(zcu)) {
194 .no_possible_value => .one_possible_value,
195 .one_possible_value => .runtime,
196 else => |class| class,
195 .opt_type => |child_ty_ip| {
196 extra_states = switch (extra_states) {
197 .none => .one,
198 .one, .many => .many,
199 };
200 cur_ty = .fromInterned(child_ty_ip);
201 continue;
197202 },
198203 .tuple_type => |tuple| {
199 var has_runtime_state = false;
200 var has_comptime_state = false;
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 }
204 @branchHint(.unlikely);
205 break classifyTuple(tuple.types.get(ip), tuple.values.get(ip), zcu);
219206 },
220207 .struct_type => {
221 const struct_obj = ip.loadStructType(ty.toIntern());
222 return switch (struct_obj.layout) {
223 .auto, .@"extern" => struct_obj.class,
224 .@"packed" => Type.fromInterned(struct_obj.packed_backing_int_type).classify(zcu),
225 };
208 const struct_obj = ip.loadStructType(cur_ty.toIntern());
209 switch (struct_obj.layout) {
210 .auto, .@"extern" => {
211 zcu.assertUpToDate(.wrap(.{ .type_layout = cur_ty.toIntern() }));
212 break struct_obj.class;
213 },
214 .@"packed" => {
215 cur_ty = .fromInterned(struct_obj.packed_backing_int_type);
216 continue;
217 },
218 }
226219 },
227220 .union_type => {
228 const union_obj = ip.loadUnionType(ty.toIntern());
229 return switch (union_obj.layout) {
230 .auto, .@"extern" => union_obj.class,
231 .@"packed" => Type.fromInterned(union_obj.packed_backing_int_type).classify(zcu),
232 };
221 const union_obj = ip.loadUnionType(cur_ty.toIntern());
222 switch (union_obj.layout) {
223 .auto, .@"extern" => {
224 zcu.assertUpToDate(.wrap(.{ .type_layout = cur_ty.toIntern() }));
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;
233237 },
234 .enum_type => Type.fromInterned(ip.loadEnumType(ty.toIntern()).int_tag_type).classify(zcu),
235238
236239 // values, not types
237240 .undef,
......@@ -255,6 +258,53 @@ pub fn classify(ty: Type, zcu: *const Zcu) Class {
255258 .memoized_call,
256259 => unreachable,
257260 };
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 }
258308}
259309
260310/// Asserts the type is resolved.
......@@ -1061,7 +1111,10 @@ pub fn abiSize(ty: Type, zcu: *const Zcu) u64 {
10611111 .error_union_type => |error_union| {
10621112 const payload_ty: Type = .fromInterned(error_union.payload_type);
10631113 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,
10651118 else => {},
10661119 }
10671120 // The layout will either be (code, payload, padding) or (payload, code, padding)
......@@ -3177,6 +3230,12 @@ fn validateExternCallconv(cc: std.builtin.CallingConvention) bool {
31773230
31783231/// Asserts that `ty` has resolved layout.
31793232pub 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 }
31803239 switch (zcu.intern_pool.indexToKey(ty.toIntern())) {
31813240 .int_type,
31823241 .ptr_type,