authorgravatar for robin@voetter.nlRobin Voetter <robin@voetter.nl> 2024-01-20 23:57:19+01:00
committergravatar for robin@voetter.nlRobin Voetter <robin@voetter.nl> 2024-02-04 19:09:28+01:00
log54ec9365498635aa127ff13dfbdd3942890b53d0
treea6df4f0650bc8972746fa4c1636ee55af4143fef
parentb67d983abda198c69fbcde68a961e0e8b92b7939
signaturebadge-check Signed by SSH key SHA256:ZS52FNyUv2WUXvO4njmVaFVO46RHojFuOrxRc4LuKzg

spirv: wrap strange its before instead of after operation

Wrapping strange integers before an operation was initially done as an attempt to minimize the amount of normalizations required: This way, there would not be a normalization necessary between two modular operations. This was a premature optimization, since the resulting logic is more complicated than naive way of wrapping the result after the operation. This commit updates handling of strange integers to do wrapping after each operation. It also seems slightly more efficient in terms of size of generated code, as it reduces the size of the behavior tests binary by about 1%.

1 files changed, 114 insertions(+), 119 deletions(-)

src/codegen/spirv.zig+114-119
......@@ -2167,21 +2167,20 @@ const DeclGen = struct {
21672167 const air_tags = self.air.instructions.items(.tag);
21682168 const maybe_result_id: ?IdRef = switch (air_tags[@intFromEnum(inst)]) {
21692169 // zig fmt: off
2170 .add, .add_wrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd, true),
2171 .sub, .sub_wrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub, true),
2172 .mul, .mul_wrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul, true),
2170 .add, .add_wrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd),
2171 .sub, .sub_wrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub),
2172 .mul, .mul_wrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul),
21732173
21742174 .div_float,
21752175 .div_float_optimized,
21762176 // TODO: Check that this is the right operation.
21772177 .div_trunc,
21782178 .div_trunc_optimized,
2179 => try self.airArithOp(inst, .OpFDiv, .OpSDiv, .OpUDiv, false),
2179 => try self.airArithOp(inst, .OpFDiv, .OpSDiv, .OpUDiv),
21802180 // TODO: Check if this is the right operation
2181 // TODO: Make airArithOp for rem not emit a mask for the LHS.
21822181 .rem,
21832182 .rem_optimized,
2184 => try self.airArithOp(inst, .OpFRem, .OpSRem, .OpSRem, false),
2183 => try self.airArithOp(inst, .OpFRem, .OpSRem, .OpSRem),
21852184
21862185 .add_with_overflow => try self.airAddSubOverflow(inst, .OpIAdd, .OpULessThan, .OpSLessThan),
21872186 .sub_with_overflow => try self.airAddSubOverflow(inst, .OpISub, .OpUGreaterThan, .OpSGreaterThan),
......@@ -2346,13 +2345,10 @@ const DeclGen = struct {
23462345
23472346 var wip = try self.elementWise(result_ty);
23482347 defer wip.deinit();
2349 for (0..wip.results.len) |i| {
2348 for (wip.results, 0..) |*result_id, i| {
23502349 const lhs_elem_id = try wip.elementAt(result_ty, lhs_id, i);
23512350 const rhs_elem_id = try wip.elementAt(shift_ty, rhs_id, i);
23522351
2353 // TODO: Can we omit normalizing lhs?
2354 const lhs_norm_id = try self.normalizeInt(wip.scalar_ty_ref, lhs_elem_id, info);
2355
23562352 // Sometimes Zig doesn't make both of the arguments the same types here. SPIR-V expects that,
23572353 // so just manually upcast it if required.
23582354 const shift_id = if (scalar_shift_ty_ref != wip.scalar_ty_ref) blk: {
......@@ -2364,13 +2360,13 @@ const DeclGen = struct {
23642360 });
23652361 break :blk shift_id;
23662362 } else rhs_elem_id;
2367 const shift_norm_id = try self.normalizeInt(wip.scalar_ty_ref, shift_id, info);
23682363
2364 const value_id = self.spv.allocId();
23692365 const args = .{
23702366 .id_result_type = wip.scalar_ty_id,
2371 .id_result = wip.allocId(i),
2372 .base = lhs_norm_id,
2373 .shift = shift_norm_id,
2367 .id_result = value_id,
2368 .base = lhs_elem_id,
2369 .shift = shift_id,
23742370 };
23752371
23762372 if (result_ty.isSignedInt(mod)) {
......@@ -2378,6 +2374,8 @@ const DeclGen = struct {
23782374 } else {
23792375 try self.func.body.emit(self.spv.gpa, unsigned, args);
23802376 }
2377
2378 result_id.* = try self.normalize(wip.scalar_ty_ref, value_id, info);
23812379 }
23822380 return try wip.finalize();
23832381 }
......@@ -2435,47 +2433,52 @@ const DeclGen = struct {
24352433 return result_id;
24362434 }
24372435
2438 /// This function canonicalizes a "strange" integer value:
2439 /// For unsigned integers, the value is masked so that only the relevant bits can contain
2440 /// non-zeros.
2441 /// For signed integers, the value is also sign extended.
2442 fn normalizeInt(self: *DeclGen, ty_ref: CacheRef, value_id: IdRef, info: ArithmeticTypeInfo) !IdRef {
2443 assert(info.class != .composite_integer); // TODO
2444 if (info.bits == info.backing_bits) {
2445 return value_id;
2446 }
2447
2448 switch (info.signedness) {
2449 .unsigned => {
2450 const mask_value = if (info.bits == 64) 0xFFFF_FFFF_FFFF_FFFF else (@as(u64, 1) << @as(u6, @intCast(info.bits))) - 1;
2451 const result_id = self.spv.allocId();
2452 const mask_id = try self.constInt(ty_ref, mask_value);
2453 try self.func.body.emit(self.spv.gpa, .OpBitwiseAnd, .{
2454 .id_result_type = self.typeId(ty_ref),
2455 .id_result = result_id,
2456 .operand_1 = value_id,
2457 .operand_2 = mask_id,
2458 });
2459 return result_id;
2460 },
2461 .signed => {
2462 // Shift left and right so that we can copy the sight bit that way.
2463 const shift_amt_id = try self.constInt(ty_ref, info.backing_bits - info.bits);
2464 const left_id = self.spv.allocId();
2465 try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
2466 .id_result_type = self.typeId(ty_ref),
2467 .id_result = left_id,
2468 .base = value_id,
2469 .shift = shift_amt_id,
2470 });
2471 const right_id = self.spv.allocId();
2472 try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
2473 .id_result_type = self.typeId(ty_ref),
2474 .id_result = right_id,
2475 .base = left_id,
2476 .shift = shift_amt_id,
2477 });
2478 return right_id;
2436 /// This function normalizes values to a canonical representation
2437 /// after some arithmetic operation. This mostly consists of wrapping
2438 /// behavior for strange integers:
2439 /// - Unsigned integers are bitwise masked with a mask that only passes
2440 /// the valid bits through.
2441 /// - Signed integers are also sign extended if they are negative.
2442 /// All other values are returned unmodified (this makes strange integer
2443 /// wrapping easier to use in generic operations).
2444 fn normalize(self: *DeclGen, ty_ref: CacheRef, value_id: IdRef, info: ArithmeticTypeInfo) !IdRef {
2445 switch (info.class) {
2446 .integer, .bool, .float => return value_id,
2447 .composite_integer => unreachable, // TODO
2448 .strange_integer => {
2449 switch (info.signedness) {
2450 .unsigned => {
2451 const mask_value = if (info.bits == 64) 0xFFFF_FFFF_FFFF_FFFF else (@as(u64, 1) << @as(u6, @intCast(info.bits))) - 1;
2452 const result_id = self.spv.allocId();
2453 const mask_id = try self.constInt(ty_ref, mask_value);
2454 try self.func.body.emit(self.spv.gpa, .OpBitwiseAnd, .{
2455 .id_result_type = self.typeId(ty_ref),
2456 .id_result = result_id,
2457 .operand_1 = value_id,
2458 .operand_2 = mask_id,
2459 });
2460 return result_id;
2461 },
2462 .signed => {
2463 // Shift left and right so that we can copy the sight bit that way.
2464 const shift_amt_id = try self.constInt(ty_ref, info.backing_bits - info.bits);
2465 const left_id = self.spv.allocId();
2466 try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
2467 .id_result_type = self.typeId(ty_ref),
2468 .id_result = left_id,
2469 .base = value_id,
2470 .shift = shift_amt_id,
2471 });
2472 const right_id = self.spv.allocId();
2473 try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
2474 .id_result_type = self.typeId(ty_ref),
2475 .id_result = right_id,
2476 .base = left_id,
2477 .shift = shift_amt_id,
2478 });
2479 return right_id;
2480 },
2481 }
24792482 },
24802483 }
24812484 }
......@@ -2486,8 +2489,6 @@ const DeclGen = struct {
24862489 comptime fop: Opcode,
24872490 comptime sop: Opcode,
24882491 comptime uop: Opcode,
2489 /// true if this operation holds under modular arithmetic.
2490 comptime modular: bool,
24912492 ) !?IdRef {
24922493 if (self.liveness.isUnused(inst)) return null;
24932494
......@@ -2501,7 +2502,7 @@ const DeclGen = struct {
25012502 assert(self.typeOf(bin_op.lhs).eql(ty, self.module));
25022503 assert(self.typeOf(bin_op.rhs).eql(ty, self.module));
25032504
2504 return try self.arithOp(ty, lhs_id, rhs_id, fop, sop, uop, modular);
2505 return try self.arithOp(ty, lhs_id, rhs_id, fop, sop, uop);
25052506 }
25062507
25072508 fn arithOp(
......@@ -2512,8 +2513,6 @@ const DeclGen = struct {
25122513 comptime fop: Opcode,
25132514 comptime sop: Opcode,
25142515 comptime uop: Opcode,
2515 /// true if this operation holds under modular arithmetic.
2516 comptime modular: bool,
25172516 ) !IdRef {
25182517 // Binary operations are generally applicable to both scalar and vector operations
25192518 // in SPIR-V, but int and float versions of operations require different opcodes.
......@@ -2533,25 +2532,16 @@ const DeclGen = struct {
25332532
25342533 var wip = try self.elementWise(ty);
25352534 defer wip.deinit();
2536 for (0..wip.results.len) |i| {
2535 for (wip.results, 0..) |*result_id, i| {
25372536 const lhs_elem_id = try wip.elementAt(ty, lhs_id, i);
25382537 const rhs_elem_id = try wip.elementAt(ty, rhs_id, i);
25392538
2540 const lhs_norm_id = if (modular and info.class == .strange_integer)
2541 try self.normalizeInt(wip.scalar_ty_ref, lhs_elem_id, info)
2542 else
2543 lhs_elem_id;
2544
2545 const rhs_norm_id = if (modular and info.class == .strange_integer)
2546 try self.normalizeInt(wip.scalar_ty_ref, rhs_elem_id, info)
2547 else
2548 rhs_elem_id;
2549
2539 const value_id = self.spv.allocId();
25502540 const operands = .{
25512541 .id_result_type = wip.scalar_ty_id,
2552 .id_result = wip.allocId(i),
2553 .operand_1 = lhs_norm_id,
2554 .operand_2 = rhs_norm_id,
2542 .id_result = value_id,
2543 .operand_1 = lhs_elem_id,
2544 .operand_2 = rhs_elem_id,
25552545 };
25562546
25572547 switch (opcode_index) {
......@@ -2563,6 +2553,7 @@ const DeclGen = struct {
25632553
25642554 // TODO: Trap on overflow? Probably going to be annoying.
25652555 // TODO: Look into SPV_KHR_no_integer_wrap_decoration which provides NoSignedWrap/NoUnsignedWrap.
2556 result_id.* = try self.normalize(wip.scalar_ty_ref, value_id, info);
25662557 }
25672558
25682559 return try wip.finalize();
......@@ -2599,24 +2590,22 @@ const DeclGen = struct {
25992590 defer wip_result.deinit();
26002591 var wip_ov = try self.elementWise(ov_ty);
26012592 defer wip_ov.deinit();
2602 for (wip_result.results, wip_ov.results, 0..) |*value_id, *ov_id, i| {
2593 for (wip_result.results, wip_ov.results, 0..) |*result_id, *ov_id, i| {
26032594 const lhs_elem_id = try wip_result.elementAt(operand_ty, lhs, i);
26042595 const rhs_elem_id = try wip_result.elementAt(operand_ty, rhs, i);
26052596
26062597 // Normalize both so that we can properly check for overflow
2607 const lhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, lhs_elem_id, info);
2608 const rhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, rhs_elem_id, info);
2609 const op_result_id = self.spv.allocId();
2598 const value_id = self.spv.allocId();
26102599
26112600 try self.func.body.emit(self.spv.gpa, add, .{
26122601 .id_result_type = wip_result.scalar_ty_id,
2613 .id_result = op_result_id,
2614 .operand_1 = lhs_norm_id,
2615 .operand_2 = rhs_norm_id,
2602 .id_result = value_id,
2603 .operand_1 = lhs_elem_id,
2604 .operand_2 = rhs_elem_id,
26162605 });
26172606
26182607 // Normalize the result so that the comparisons go well
2619 value_id.* = try self.normalizeInt(wip_result.scalar_ty_ref, op_result_id, info);
2608 result_id.* = try self.normalize(wip_result.scalar_ty_ref, value_id, info);
26202609
26212610 const overflowed_id = switch (info.signedness) {
26222611 .unsigned => blk: {
......@@ -2626,8 +2615,8 @@ const DeclGen = struct {
26262615 try self.func.body.emit(self.spv.gpa, ucmp, .{
26272616 .id_result_type = self.typeId(bool_ty_ref),
26282617 .id_result = overflowed_id,
2629 .operand_1 = value_id.*,
2630 .operand_2 = lhs_norm_id,
2618 .operand_1 = result_id.*,
2619 .operand_2 = lhs_elem_id,
26312620 });
26322621 break :blk overflowed_id;
26332622 },
......@@ -2654,7 +2643,7 @@ const DeclGen = struct {
26542643 try self.func.body.emit(self.spv.gpa, .OpSLessThan, .{
26552644 .id_result_type = self.typeId(bool_ty_ref),
26562645 .id_result = rhs_lt_zero_id,
2657 .operand_1 = rhs_norm_id,
2646 .operand_1 = rhs_elem_id,
26582647 .operand_2 = zero_id,
26592648 });
26602649
......@@ -2662,8 +2651,8 @@ const DeclGen = struct {
26622651 try self.func.body.emit(self.spv.gpa, scmp, .{
26632652 .id_result_type = self.typeId(bool_ty_ref),
26642653 .id_result = value_gt_lhs_id,
2665 .operand_1 = lhs_norm_id,
2666 .operand_2 = value_id.*,
2654 .operand_1 = lhs_elem_id,
2655 .operand_2 = result_id.*,
26672656 });
26682657
26692658 const overflowed_id = self.spv.allocId();
......@@ -2715,13 +2704,10 @@ const DeclGen = struct {
27152704 defer wip_result.deinit();
27162705 var wip_ov = try self.elementWise(ov_ty);
27172706 defer wip_ov.deinit();
2718 for (0..wip_result.results.len, wip_ov.results) |i, *ov_id| {
2707 for (wip_result.results, wip_ov.results, 0..) |*result_id, *ov_id, i| {
27192708 const lhs_elem_id = try wip_result.elementAt(operand_ty, lhs, i);
27202709 const rhs_elem_id = try wip_result.elementAt(shift_ty, rhs, i);
27212710
2722 // Normalize both so that we can shift back and check if the result is the same.
2723 const lhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, lhs_elem_id, info);
2724
27252711 // Sometimes Zig doesn't make both of the arguments the same types here. SPIR-V expects that,
27262712 // so just manually upcast it if required.
27272713 const shift_id = if (scalar_shift_ty_ref != wip_result.scalar_ty_ref) blk: {
......@@ -2733,29 +2719,41 @@ const DeclGen = struct {
27332719 });
27342720 break :blk shift_id;
27352721 } else rhs_elem_id;
2736 const shift_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, shift_id, info);
27372722
2723 const value_id = self.spv.allocId();
27382724 try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
27392725 .id_result_type = wip_result.scalar_ty_id,
2740 .id_result = wip_result.allocId(i),
2741 .base = lhs_norm_id,
2742 .shift = shift_norm_id,
2726 .id_result = value_id,
2727 .base = lhs_elem_id,
2728 .shift = shift_id,
27432729 });
2730 result_id.* = try self.normalize(wip_result.scalar_ty_ref, value_id, info);
27442731
2745 // To check if overflow happened, just check if the right-shifted result is the same value.
27462732 const right_shift_id = self.spv.allocId();
2747 try self.func.body.emit(self.spv.gpa, .OpShiftRightLogical, .{
2748 .id_result_type = wip_result.scalar_ty_id,
2749 .id_result = right_shift_id,
2750 .base = try self.normalizeInt(wip_result.scalar_ty_ref, wip_result.results[i], info),
2751 .shift = shift_norm_id,
2752 });
2733 switch (info.signedness) {
2734 .signed => {
2735 try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
2736 .id_result_type = wip_result.scalar_ty_id,
2737 .id_result = right_shift_id,
2738 .base = result_id.*,
2739 .shift = shift_id,
2740 });
2741 },
2742 .unsigned => {
2743 try self.func.body.emit(self.spv.gpa, .OpShiftRightLogical, .{
2744 .id_result_type = wip_result.scalar_ty_id,
2745 .id_result = right_shift_id,
2746 .base = result_id.*,
2747 .shift = shift_id,
2748 });
2749 },
2750 }
27532751
27542752 const overflowed_id = self.spv.allocId();
27552753 try self.func.body.emit(self.spv.gpa, .OpINotEqual, .{
27562754 .id_result_type = self.typeId(bool_ty_ref),
27572755 .id_result = overflowed_id,
2758 .operand_1 = lhs_norm_id,
2756 .operand_1 = lhs_elem_id,
27592757 .operand_2 = right_shift_id,
27602758 });
27612759
......@@ -3113,14 +3111,7 @@ const DeclGen = struct {
31133111 .neq => .OpLogicalNotEqual,
31143112 else => unreachable,
31153113 },
3116 .strange_integer => sign: {
3117 const op_ty_ref = try self.resolveType(op_ty, .direct);
3118 // Mask operands before performing comparison.
3119 cmp_lhs_id = try self.normalizeInt(op_ty_ref, cmp_lhs_id, info);
3120 cmp_rhs_id = try self.normalizeInt(op_ty_ref, cmp_rhs_id, info);
3121 break :sign info.signedness;
3122 },
3123 .integer => info.signedness,
3114 .integer, .strange_integer => info.signedness,
31243115 };
31253116
31263117 break :opcode switch (signedness) {
......@@ -3252,18 +3243,13 @@ const DeclGen = struct {
32523243 const operand_id = try self.resolve(ty_op.operand);
32533244 const src_ty = self.typeOf(ty_op.operand);
32543245 const dst_ty = self.typeOfIndex(inst);
3255 const src_ty_ref = try self.resolveType(src_ty, .direct);
32563246 const dst_ty_ref = try self.resolveType(dst_ty, .direct);
32573247
32583248 const src_info = try self.arithmeticTypeInfo(src_ty);
32593249 const dst_info = try self.arithmeticTypeInfo(dst_ty);
32603250
3261 // While intcast promises that the value already fits, the upper bits of a
3262 // strange integer may contain garbage. Therefore, mask/sign extend it before.
3263 const src_id = try self.normalizeInt(src_ty_ref, operand_id, src_info);
3264
32653251 if (src_info.backing_bits == dst_info.backing_bits) {
3266 return src_id;
3252 return operand_id;
32673253 }
32683254
32693255 const result_id = self.spv.allocId();
......@@ -3271,14 +3257,23 @@ const DeclGen = struct {
32713257 .signed => try self.func.body.emit(self.spv.gpa, .OpSConvert, .{
32723258 .id_result_type = self.typeId(dst_ty_ref),
32733259 .id_result = result_id,
3274 .signed_value = src_id,
3260 .signed_value = operand_id,
32753261 }),
32763262 .unsigned => try self.func.body.emit(self.spv.gpa, .OpUConvert, .{
32773263 .id_result_type = self.typeId(dst_ty_ref),
32783264 .id_result = result_id,
3279 .unsigned_value = src_id,
3265 .unsigned_value = operand_id,
32803266 }),
32813267 }
3268
3269 // Make sure to normalize the result if shrinking.
3270 // Because strange ints are sign extended in their backing
3271 // type, we don't need to normalize when growing the type. The
3272 // representation is already the same.
3273 if (dst_info.bits < src_info.bits) {
3274 return try self.normalize(dst_ty_ref, result_id, dst_info);
3275 }
3276
32823277 return result_id;
32833278 }
32843279