diff --git a/src/Quantity.zig b/src/Quantity.zig index be358ce..96c138c 100644 --- a/src/Quantity.zig +++ b/src/Quantity.zig @@ -652,3 +652,139 @@ test "Overhead Analysis: Quantity vs Native" { std.debug.print("└───────────┴──────┴───────────┴───────────┴───────────┘\n", .{}); try std.testing.expect(gsink != 0); } + +test "Cross-Type Overhead Analysis: Quantity vs Native" { + const Io = std.Io; + const ITERS: usize = 100_000; + const SAMPLES: usize = 5; + const io = std.testing.io; + + const getTime = struct { + fn f(i: Io) Io.Timestamp { + return Io.Clock.awake.now(i); + } + }.f; + + const fold = struct { + fn f(comptime TT: type, s: *f64, v: TT) void { + s.* += if (comptime @typeInfo(TT) == .float) + @as(f64, @floatCast(v)) + else + @as(f64, @floatFromInt(v)); + } + }.f; + + const getValT = struct { + fn f(comptime TT: type, i: usize) TT { + // Keep values safe and non-zero to avoid division by zero or overflows during cross-casting + const v = (i % 50) + 1; + return if (comptime @typeInfo(TT) == .float) @floatFromInt(v) else @intCast(v); + } + }.f; + + // Helper for the Native baseline: explicitly casting T2 to T1 before the operation + const castTo = struct { + fn f(comptime DestT: type, comptime SrcT: type, val: SrcT) DestT { + if (comptime DestT == SrcT) return val; + const src_info = @typeInfo(SrcT); + const dest_info = @typeInfo(DestT); + + if (dest_info == .int and src_info == .int) return @intCast(val); + if (dest_info == .float and src_info == .int) return @floatFromInt(val); + if (dest_info == .int and src_info == .float) return @intFromFloat(val); + if (dest_info == .float and src_info == .float) return @floatCast(val); + unreachable; + } + }.f; + + const Types = .{ i16, i64, i128, f32, f64 }; + const TNames = .{ "i16", "i64", "i128", "f32", "f64" }; + const Ops = .{ "add", "mulBy", "divBy" }; + + var gsink: f64 = 0; + + std.debug.print( + \\ + \\ Cross-Type Overhead Analysis: Quantity vs Native + \\ + \\┌─────────┬──────┬──────┬───────────┬───────────┬───────────┐ + \\│ Op │ T1 │ T2 │ Native │ Quantity │ Slowdown │ + \\├─────────┼──────┼──────┼───────────┼───────────┼───────────┤ + \\ + , .{}); + + inline for (Ops, 0..) |op_name, j| { + inline for (Types, 0..) |T1, t1_idx| { + inline for (Types, 0..) |T2, t2_idx| { + var native_total_ns: f64 = 0; + var quantity_total_ns: f64 = 0; + + const M1 = Quantity(T1, Dimensions.init(.{ .L = 1 }), Scales.init(.{})); + const M2 = Quantity(T2, Dimensions.init(.{ .L = 1 }), Scales.init(.{})); + const S2 = Quantity(T2, Dimensions.init(.{ .T = 1 }), Scales.init(.{})); + + for (0..SAMPLES) |_| { + // --- 1. Benchmark Native (Cast T2 to T1, then math) --- + var n_sink: T1 = 0; + const n_start = getTime(io); + for (0..ITERS) |i| { + const a = getValT(T1, i); + const b_raw = getValT(T2, 2); + const b = castTo(T1, T2, b_raw); + + const r = if (comptime std.mem.eql(u8, op_name, "add")) + a + b + else if (comptime std.mem.eql(u8, op_name, "mulBy")) + a * b + else if (comptime @typeInfo(T1) == .int) + @divTrunc(a, b) + else + a / b; + + if (comptime @typeInfo(T1) == .float) n_sink += r else n_sink ^= r; + } + const n_end = getTime(io); + native_total_ns += @as(f64, @floatFromInt(n_start.durationTo(n_end).toNanoseconds())); + fold(T1, &gsink, n_sink); + + // --- 2. Benchmark Quantity --- + var q_sink: T1 = 0; + const q_start = getTime(io); + for (0..ITERS) |i| { + const qa = M1{ .value = getValT(T1, i) }; + const qb = if (comptime std.mem.eql(u8, op_name, "divBy")) + S2{ .value = getValT(T2, 2) } + else + M2{ .value = getValT(T2, 2) }; + + const r = if (comptime std.mem.eql(u8, op_name, "add")) + qa.add(qb) + else if (comptime std.mem.eql(u8, op_name, "mulBy")) + qa.mulBy(qb) + else + qa.divBy(qb); + + if (comptime @typeInfo(T1) == .float) q_sink += r.value else q_sink ^= r.value; + } + const q_end = getTime(io); + quantity_total_ns += @as(f64, @floatFromInt(q_start.durationTo(q_end).toNanoseconds())); + fold(T1, &gsink, q_sink); + } + + const avg_n = (native_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS)); + const avg_q = (quantity_total_ns / SAMPLES) / @as(f64, @floatFromInt(ITERS)); + const slowdown = avg_q / avg_n; + + std.debug.print("│ {s:<7} │ {s:<4} │ {s:<4} │ {d:>7.2}ns │ {d:>7.2}ns │ {d:>8.2}x │\n", .{ + op_name, TNames[t1_idx], TNames[t2_idx], avg_n, avg_q, slowdown, + }); + } + } + if (j != Ops.len - 1) { + std.debug.print("├─────────┼──────┼──────┼───────────┼───────────┼───────────┤\n", .{}); + } + } + + std.debug.print("└─────────┴──────┴──────┴───────────┴───────────┴───────────┘\n", .{}); + try std.testing.expect(gsink != 0); +}