authorgravatar for abbix@riseup.netlg <abbix@riseup.net> 2026-08-31 11:48:29+02:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-08-31 11:48:29+02:00
log749ed5fc7f4afd7a42830f77f69245f1abcdd9a0
treecd34e3987a5188f8a05c9bb98002f91bf05cbedd
parentaad8da82a5cfd9b7602ee3adbff340c9c15d9165

Sema: comptime arithmetic optimizations (#36495)

Tests heavy on comptime arithmetic show a ~7% improvement in wall-clock time, and a ~11% improvement in peak RSS. There are two optimizations: 1. Avoid interning the overflow bit Previously, the overflow bit was being interned and immediately read back. Instead, a new OverflowArithmeticResultInt type is introduced that contains the overflow result as a boolean directly. When converted, that boolean points to the already existing zero_u1 and one_u1 interned values. 2. Try arithmetic operations without BigInt first. This did not show significant performance improvements, but it did show a significant memory usage improvement, almost going 0.5x! Instead of resorting to BigInt first, arithmetic operations are tried in a u64. Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36495 Reviewed-by: mlugg <mlugg@mlugg.co.uk>

2 files changed, 79 insertions(+), 43 deletions(-)

src/Sema/arith.zig+74-43
......@@ -169,7 +169,11 @@ fn addWithOverflowScalar(
169169 .overflow_bit = .undef_u1,
170170 .wrapped_result = try pt.undefValue(ty),
171171 };
172 return intAddWithOverflow(sema, lhs, rhs, ty);
172 const res = try intAddWithOverflow(sema, lhs, rhs, ty);
173 return .{
174 .overflow_bit = if (res.overflow) .one_u1 else .zero_u1,
175 .wrapped_result = res.wrapped_result,
176 };
173177}
174178
175179/// `lhs` and `rhs` are of type `ty`.
......@@ -227,7 +231,12 @@ fn subWithOverflowScalar(
227231 .overflow_bit = .undef_u1,
228232 .wrapped_result = try pt.undefValue(ty),
229233 };
230 return intSubWithOverflow(sema, lhs, rhs, ty);
234
235 const res = try intSubWithOverflow(sema, lhs, rhs, ty);
236 return .{
237 .overflow_bit = if (res.overflow) .one_u1 else .zero_u1,
238 .wrapped_result = res.wrapped_result,
239 };
231240}
232241
233242/// `lhs` and `rhs` are of type `ty`.
......@@ -285,7 +294,11 @@ fn mulWithOverflowScalar(
285294 .overflow_bit = .undef_u1,
286295 .wrapped_result = try pt.undefValue(ty),
287296 };
288 return intMulWithOverflow(sema, lhs, rhs, ty);
297 const res = try intMulWithOverflow(sema, lhs, rhs, ty);
298 return .{
299 .overflow_bit = if (res.overflow) .one_u1 else .zero_u1,
300 .wrapped_result = res.wrapped_result,
301 };
289302}
290303
291304/// Applies the `+` operator to comptime-known values.
......@@ -1108,7 +1121,7 @@ fn shlWithOverflowScalar(
11081121 .int => {
11091122 const result = try intShlWithOverflow(sema, block, lhs_ty, lhs_val, rhs_val, rhs_src, true, vec_idx);
11101123 return .{
1111 .overflow_bit = try pt.intValue(.u1, @intFromBool(result.overflow)),
1124 .overflow_bit = if (result.overflow) .one_u1 else .zero_u1,
11121125 .wrapped_result = result.val,
11131126 };
11141127 },
......@@ -1390,17 +1403,12 @@ pub fn byteSwap(sema: *Sema, val: Value, ty: Type) CompileError!Value {
13901403/// If the value overflowed the type, returns a comptime_int instead.
13911404/// Only supports scalars.
13921405fn intAdd(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: bool, val: Value } {
1393 const pt = sema.pt;
1394 const zcu = pt.zcu;
13951406 switch (ty.toIntern()) {
13961407 .comptime_int_type => return .{ .overflow = false, .val = try comptimeIntAdd(sema, lhs, rhs) },
13971408 else => {
13981409 const res = try intAddWithOverflowInner(sema, lhs, rhs, ty);
1399 return switch (res.overflow_bit.toUnsignedInt(zcu)) {
1400 0 => .{ .overflow = false, .val = res.wrapped_result },
1401 1 => .{ .overflow = true, .val = try comptimeIntAdd(sema, lhs, rhs) },
1402 else => unreachable,
1403 };
1410 if (res.overflow) return .{ .overflow = true, .val = try comptimeIntAdd(sema, lhs, rhs) };
1411 return .{ .overflow = false, .val = res.wrapped_result };
14041412 },
14051413 }
14061414}
......@@ -1408,8 +1416,19 @@ fn intAdd(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: boo
14081416fn comptimeIntAdd(sema: *Sema, lhs: Value, rhs: Value) !Value {
14091417 const pt = sema.pt;
14101418 const zcu = pt.zcu;
1411 // TODO is this a performance issue? maybe we should try the operation without
1412 // resorting to BigInt first.
1419
1420 // Try the operation without resorting to BigInt first.
1421 if (lhs.getUnsignedInt(zcu)) |lhs_val| {
1422 if (rhs.getUnsignedInt(zcu)) |rhs_val| {
1423 const result, const overflow = @addWithOverflow(lhs_val, rhs_val);
1424
1425 if (overflow == 0) {
1426 return pt.intValue(.comptime_int, result);
1427 }
1428 // It would overflow, fall back to BigInt.
1429 }
1430 }
1431
14131432 var lhs_space: Value.BigIntSpace = undefined;
14141433 var rhs_space: Value.BigIntSpace = undefined;
14151434 const lhs_bigint = lhs.toBigInt(&lhs_space, zcu);
......@@ -1422,17 +1441,17 @@ fn comptimeIntAdd(sema: *Sema, lhs: Value, rhs: Value) !Value {
14221441 result_bigint.add(lhs_bigint, rhs_bigint);
14231442 return pt.intValue_big(.comptime_int, result_bigint.toConst());
14241443}
1425fn intAddWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1444fn intAddWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
14261445 switch (ty.toIntern()) {
14271446 .comptime_int_type => return .{
1428 .overflow_bit = .zero_u1,
1447 .overflow = false,
14291448 .wrapped_result = try comptimeIntAdd(sema, lhs, rhs),
14301449 },
14311450 else => return intAddWithOverflowInner(sema, lhs, rhs, ty),
14321451 }
14331452}
14341453/// Like `intAddWithOverflow`, but asserts that `ty` is not `Type.comptime_int`.
1435fn intAddWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1454fn intAddWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
14361455 assert(ty.toIntern() != .comptime_int_type);
14371456 const pt = sema.pt;
14381457 const zcu = pt.zcu;
......@@ -1448,7 +1467,7 @@ fn intAddWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value
14481467 var result_bigint: BigIntMutable = .{ .limbs = limbs, .positive = undefined, .len = undefined };
14491468 const overflowed = result_bigint.addWrap(lhs_bigint, rhs_bigint, info.signedness, info.bits);
14501469 return .{
1451 .overflow_bit = try pt.intValue(.u1, @intFromBool(overflowed)),
1470 .overflow = overflowed,
14521471 .wrapped_result = try pt.intValue_big(ty, result_bigint.toConst()),
14531472 };
14541473}
......@@ -1472,17 +1491,12 @@ fn intAddSat(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value {
14721491/// If the value overflowed the type, returns a comptime_int instead.
14731492/// Only supports scalars.
14741493fn intSub(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: bool, val: Value } {
1475 const pt = sema.pt;
1476 const zcu = pt.zcu;
14771494 switch (ty.toIntern()) {
14781495 .comptime_int_type => return .{ .overflow = false, .val = try comptimeIntSub(sema, lhs, rhs) },
14791496 else => {
14801497 const res = try intSubWithOverflowInner(sema, lhs, rhs, ty);
1481 return switch (res.overflow_bit.toUnsignedInt(zcu)) {
1482 0 => .{ .overflow = false, .val = res.wrapped_result },
1483 1 => .{ .overflow = true, .val = try comptimeIntSub(sema, lhs, rhs) },
1484 else => unreachable,
1485 };
1498 if (res.overflow) return .{ .overflow = true, .val = try comptimeIntSub(sema, lhs, rhs) };
1499 return .{ .overflow = false, .val = res.wrapped_result };
14861500 },
14871501 }
14881502}
......@@ -1490,8 +1504,19 @@ fn intSub(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: boo
14901504fn comptimeIntSub(sema: *Sema, lhs: Value, rhs: Value) !Value {
14911505 const pt = sema.pt;
14921506 const zcu = pt.zcu;
1493 // TODO is this a performance issue? maybe we should try the operation without
1494 // resorting to BigInt first.
1507
1508 // Try the operation without resorting to BigInt first.
1509 if (lhs.getUnsignedInt(zcu)) |lhs_val| {
1510 if (rhs.getUnsignedInt(zcu)) |rhs_val| {
1511 const result, const overflow = @subWithOverflow(lhs_val, rhs_val);
1512
1513 if (overflow == 0) {
1514 return pt.intValue(.comptime_int, result);
1515 }
1516 // It would overflow, fall back to BigInt.
1517 }
1518 }
1519
14951520 var lhs_space: Value.BigIntSpace = undefined;
14961521 var rhs_space: Value.BigIntSpace = undefined;
14971522 const lhs_bigint = lhs.toBigInt(&lhs_space, zcu);
......@@ -1504,17 +1529,17 @@ fn comptimeIntSub(sema: *Sema, lhs: Value, rhs: Value) !Value {
15041529 result_bigint.sub(lhs_bigint, rhs_bigint);
15051530 return pt.intValue_big(.comptime_int, result_bigint.toConst());
15061531}
1507fn intSubWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1532fn intSubWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
15081533 switch (ty.toIntern()) {
15091534 .comptime_int_type => return .{
1510 .overflow_bit = .zero_u1,
1535 .overflow = false,
15111536 .wrapped_result = try comptimeIntSub(sema, lhs, rhs),
15121537 },
15131538 else => return intSubWithOverflowInner(sema, lhs, rhs, ty),
15141539 }
15151540}
15161541/// Like `intSubWithOverflow`, but asserts that `ty` is not `Type.comptime_int`.
1517fn intSubWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1542fn intSubWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
15181543 assert(ty.toIntern() != .comptime_int_type);
15191544 const pt = sema.pt;
15201545 const zcu = pt.zcu;
......@@ -1530,7 +1555,7 @@ fn intSubWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value
15301555 var result_bigint: BigIntMutable = .{ .limbs = limbs, .positive = undefined, .len = undefined };
15311556 const overflowed = result_bigint.subWrap(lhs_bigint, rhs_bigint, info.signedness, info.bits);
15321557 return .{
1533 .overflow_bit = try pt.intValue(.u1, @intFromBool(overflowed)),
1558 .overflow = overflowed,
15341559 .wrapped_result = try pt.intValue_big(ty, result_bigint.toConst()),
15351560 };
15361561}
......@@ -1554,17 +1579,12 @@ fn intSubSat(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value {
15541579/// If the value overflowed the type, returns a comptime_int instead.
15551580/// Only supports scalars.
15561581fn intMul(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: bool, val: Value } {
1557 const pt = sema.pt;
1558 const zcu = pt.zcu;
15591582 switch (ty.toIntern()) {
15601583 .comptime_int_type => return .{ .overflow = false, .val = try comptimeIntMul(sema, lhs, rhs) },
15611584 else => {
15621585 const res = try intMulWithOverflowInner(sema, lhs, rhs, ty);
1563 return switch (res.overflow_bit.toUnsignedInt(zcu)) {
1564 0 => .{ .overflow = false, .val = res.wrapped_result },
1565 1 => .{ .overflow = true, .val = try comptimeIntMul(sema, lhs, rhs) },
1566 else => unreachable,
1567 };
1586 if (res.overflow) return .{ .overflow = true, .val = try comptimeIntMul(sema, lhs, rhs) };
1587 return .{ .overflow = false, .val = res.wrapped_result };
15681588 },
15691589 }
15701590}
......@@ -1572,8 +1592,19 @@ fn intMul(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !struct { overflow: boo
15721592fn comptimeIntMul(sema: *Sema, lhs: Value, rhs: Value) !Value {
15731593 const pt = sema.pt;
15741594 const zcu = pt.zcu;
1575 // TODO is this a performance issue? maybe we should try the operation without
1576 // resorting to BigInt first.
1595
1596 // Try the operation without resorting to BigInt first.
1597 if (lhs.getUnsignedInt(zcu)) |lhs_val| {
1598 if (rhs.getUnsignedInt(zcu)) |rhs_val| {
1599 const result, const overflow = @mulWithOverflow(lhs_val, rhs_val);
1600
1601 if (overflow == 0) {
1602 return pt.intValue(.comptime_int, result);
1603 }
1604 // It would overflow, fall back to BigInt.
1605 }
1606 }
1607
15771608 var lhs_space: Value.BigIntSpace = undefined;
15781609 var rhs_space: Value.BigIntSpace = undefined;
15791610 const lhs_bigint = lhs.toBigInt(&lhs_space, zcu);
......@@ -1590,17 +1621,17 @@ fn comptimeIntMul(sema: *Sema, lhs: Value, rhs: Value) !Value {
15901621 result_bigint.mul(lhs_bigint, rhs_bigint, limbs_buffer, sema.arena);
15911622 return pt.intValue_big(.comptime_int, result_bigint.toConst());
15921623}
1593fn intMulWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1624fn intMulWithOverflow(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
15941625 switch (ty.toIntern()) {
15951626 .comptime_int_type => return .{
1596 .overflow_bit = .zero_u1,
1627 .overflow = false,
15971628 .wrapped_result = try comptimeIntMul(sema, lhs, rhs),
15981629 },
15991630 else => return intMulWithOverflowInner(sema, lhs, rhs, ty),
16001631 }
16011632}
16021633/// Like `intMulWithOverflow`, but asserts that `ty` is not `Type.comptime_int`.
1603fn intMulWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResult {
1634fn intMulWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value.OverflowArithmeticResultInt {
16041635 const pt = sema.pt;
16051636 const zcu = pt.zcu;
16061637 const info = ty.intInfo(zcu);
......@@ -1617,7 +1648,7 @@ fn intMulWithOverflowInner(sema: *Sema, lhs: Value, rhs: Value, ty: Type) !Value
16171648 const overflowed = !result_bigint.toConst().fitsInTwosComp(info.signedness, info.bits);
16181649 if (overflowed) result_bigint.truncate(result_bigint.toConst(), info.signedness, info.bits);
16191650 return .{
1620 .overflow_bit = try pt.intValue(.u1, @intFromBool(overflowed)),
1651 .overflow = overflowed,
16211652 .wrapped_result = try pt.intValue_big(ty, result_bigint.toConst()),
16221653 };
16231654}
src/Value.zig+5
......@@ -1051,6 +1051,11 @@ pub const OverflowArithmeticResult = struct {
10511051 wrapped_result: Value,
10521052};
10531053
1054pub const OverflowArithmeticResultInt = struct {
1055 overflow: bool,
1056 wrapped_result: Value,
1057};
1058
10541059/// Supports both floats and ints; handles undefined.
10551060pub fn numberMax(lhs: Value, rhs: Value, zcu: *Zcu) Value {
10561061 if (lhs.isUndef(zcu) or rhs.isUndef(zcu)) return undef;