srctree

Robin Voetter parent b67d983a 54ec9365
spirv: wrap strange its before instead of after operation

Wrapping strange integers before an operation was initiallydone as an attempt to minimize the amount of normalizationsrequired: This way, there would not be a normalizationnecessary between two modular operations. This was apremature optimization, since the resulting logic is morecomplicated than naive way of wrapping the result afterthe operation.

This commit updates handling of strange integers to dowrapping after each operation. It also seems slightlymore efficient in terms of size of generated code, asit reduces the size of the behavior tests binary byabout 1%.

inlinesplit
src/codegen/spirv.zig added: 115, removed: 120, total 0
@@ -2167,21 +2167,20 @@ const DeclGen = struct {
const air_tags = self.air.instructions.items(.tag);
const maybe_result_id: ?IdRef = switch (air_tags[@intFromEnum(inst)]) {
// zig fmt: off
.add, .add_wrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd, true),
.sub, .sub_wrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub, true),
.mul, .mul_wrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul, true),
.add, .add_wrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd),
.sub, .sub_wrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub),
.mul, .mul_wrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul),
 
.div_float,
.div_float_optimized,
// TODO: Check that this is the right operation.
.div_trunc,
.div_trunc_optimized,
=> try self.airArithOp(inst, .OpFDiv, .OpSDiv, .OpUDiv, false),
=> try self.airArithOp(inst, .OpFDiv, .OpSDiv, .OpUDiv),
// TODO: Check if this is the right operation
// TODO: Make airArithOp for rem not emit a mask for the LHS.
.rem,
.rem_optimized,
=> try self.airArithOp(inst, .OpFRem, .OpSRem, .OpSRem, false),
=> try self.airArithOp(inst, .OpFRem, .OpSRem, .OpSRem),
 
.add_with_overflow => try self.airAddSubOverflow(inst, .OpIAdd, .OpULessThan, .OpSLessThan),
.sub_with_overflow => try self.airAddSubOverflow(inst, .OpISub, .OpUGreaterThan, .OpSGreaterThan),
@@ -2346,13 +2345,10 @@ const DeclGen = struct {
 
var wip = try self.elementWise(result_ty);
defer wip.deinit();
for (0..wip.results.len) |i| {
for (wip.results, 0..) |*result_id, i| {
const lhs_elem_id = try wip.elementAt(result_ty, lhs_id, i);
const rhs_elem_id = try wip.elementAt(shift_ty, rhs_id, i);
 
// TODO: Can we omit normalizing lhs?
const lhs_norm_id = try self.normalizeInt(wip.scalar_ty_ref, lhs_elem_id, info);
 
// Sometimes Zig doesn't make both of the arguments the same types here. SPIR-V expects that,
// so just manually upcast it if required.
const shift_id = if (scalar_shift_ty_ref != wip.scalar_ty_ref) blk: {
@@ -2364,13 +2360,13 @@ const DeclGen = struct {
});
break :blk shift_id;
} else rhs_elem_id;
const shift_norm_id = try self.normalizeInt(wip.scalar_ty_ref, shift_id, info);
 
const value_id = self.spv.allocId();
const args = .{
.id_result_type = wip.scalar_ty_id,
.id_result = wip.allocId(i),
.base = lhs_norm_id,
.shift = shift_norm_id,
.id_result = value_id,
.base = lhs_elem_id,
.shift = shift_id,
};
 
if (result_ty.isSignedInt(mod)) {
@@ -2378,6 +2374,8 @@ const DeclGen = struct {
} else {
try self.func.body.emit(self.spv.gpa, unsigned, args);
}
 
result_id.* = try self.normalize(wip.scalar_ty_ref, value_id, info);
}
return try wip.finalize();
}
@@ -2435,47 +2433,52 @@ const DeclGen = struct {
return result_id;
}
 
/// This function canonicalizes a "strange" integer value:
/// For unsigned integers, the value is masked so that only the relevant bits can contain
/// non-zeros.
/// For signed integers, the value is also sign extended.
fn normalizeInt(self: *DeclGen, ty_ref: CacheRef, value_id: IdRef, info: ArithmeticTypeInfo) !IdRef {
assert(info.class != .composite_integer); // TODO
if (info.bits == info.backing_bits) {
return value_id;
}
 
switch (info.signedness) {
.unsigned => {
const mask_value = if (info.bits == 64) 0xFFFF_FFFF_FFFF_FFFF else (@as(u64, 1) << @as(u6, @intCast(info.bits))) - 1;
const result_id = self.spv.allocId();
const mask_id = try self.constInt(ty_ref, mask_value);
try self.func.body.emit(self.spv.gpa, .OpBitwiseAnd, .{
.id_result_type = self.typeId(ty_ref),
.id_result = result_id,
.operand_1 = value_id,
.operand_2 = mask_id,
});
return result_id;
},
.signed => {
// Shift left and right so that we can copy the sight bit that way.
const shift_amt_id = try self.constInt(ty_ref, info.backing_bits - info.bits);
const left_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
.id_result_type = self.typeId(ty_ref),
.id_result = left_id,
.base = value_id,
.shift = shift_amt_id,
});
const right_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
.id_result_type = self.typeId(ty_ref),
.id_result = right_id,
.base = left_id,
.shift = shift_amt_id,
});
return right_id;
/// This function normalizes values to a canonical representation
/// after some arithmetic operation. This mostly consists of wrapping
/// behavior for strange integers:
/// - Unsigned integers are bitwise masked with a mask that only passes
/// the valid bits through.
/// - Signed integers are also sign extended if they are negative.
/// All other values are returned unmodified (this makes strange integer
/// wrapping easier to use in generic operations).
fn normalize(self: *DeclGen, ty_ref: CacheRef, value_id: IdRef, info: ArithmeticTypeInfo) !IdRef {
switch (info.class) {
.integer, .bool, .float => return value_id,
.composite_integer => unreachable, // TODO
.strange_integer => {
switch (info.signedness) {
.unsigned => {
const mask_value = if (info.bits == 64) 0xFFFF_FFFF_FFFF_FFFF else (@as(u64, 1) << @as(u6, @intCast(info.bits))) - 1;
const result_id = self.spv.allocId();
const mask_id = try self.constInt(ty_ref, mask_value);
try self.func.body.emit(self.spv.gpa, .OpBitwiseAnd, .{
.id_result_type = self.typeId(ty_ref),
.id_result = result_id,
.operand_1 = value_id,
.operand_2 = mask_id,
});
return result_id;
},
.signed => {
// Shift left and right so that we can copy the sight bit that way.
const shift_amt_id = try self.constInt(ty_ref, info.backing_bits - info.bits);
const left_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
.id_result_type = self.typeId(ty_ref),
.id_result = left_id,
.base = value_id,
.shift = shift_amt_id,
});
const right_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
.id_result_type = self.typeId(ty_ref),
.id_result = right_id,
.base = left_id,
.shift = shift_amt_id,
});
return right_id;
},
}
},
}
}
@@ -2486,8 +2489,6 @@ const DeclGen = struct {
comptime fop: Opcode,
comptime sop: Opcode,
comptime uop: Opcode,
/// true if this operation holds under modular arithmetic.
comptime modular: bool,
) !?IdRef {
if (self.liveness.isUnused(inst)) return null;
 
@@ -2501,7 +2502,7 @@ const DeclGen = struct {
assert(self.typeOf(bin_op.lhs).eql(ty, self.module));
assert(self.typeOf(bin_op.rhs).eql(ty, self.module));
 
return try self.arithOp(ty, lhs_id, rhs_id, fop, sop, uop, modular);
return try self.arithOp(ty, lhs_id, rhs_id, fop, sop, uop);
}
 
fn arithOp(
@@ -2512,8 +2513,6 @@ const DeclGen = struct {
comptime fop: Opcode,
comptime sop: Opcode,
comptime uop: Opcode,
/// true if this operation holds under modular arithmetic.
comptime modular: bool,
) !IdRef {
// Binary operations are generally applicable to both scalar and vector operations
// in SPIR-V, but int and float versions of operations require different opcodes.
@@ -2533,25 +2532,16 @@ const DeclGen = struct {
 
var wip = try self.elementWise(ty);
defer wip.deinit();
for (0..wip.results.len) |i| {
for (wip.results, 0..) |*result_id, i| {
const lhs_elem_id = try wip.elementAt(ty, lhs_id, i);
const rhs_elem_id = try wip.elementAt(ty, rhs_id, i);
 
const lhs_norm_id = if (modular and info.class == .strange_integer)
try self.normalizeInt(wip.scalar_ty_ref, lhs_elem_id, info)
else
lhs_elem_id;
 
const rhs_norm_id = if (modular and info.class == .strange_integer)
try self.normalizeInt(wip.scalar_ty_ref, rhs_elem_id, info)
else
rhs_elem_id;
 
const value_id = self.spv.allocId();
const operands = .{
.id_result_type = wip.scalar_ty_id,
.id_result = wip.allocId(i),
.operand_1 = lhs_norm_id,
.operand_2 = rhs_norm_id,
.id_result = value_id,
.operand_1 = lhs_elem_id,
.operand_2 = rhs_elem_id,
};
 
switch (opcode_index) {
@@ -2563,6 +2553,7 @@ const DeclGen = struct {
 
// TODO: Trap on overflow? Probably going to be annoying.
// TODO: Look into SPV_KHR_no_integer_wrap_decoration which provides NoSignedWrap/NoUnsignedWrap.
result_id.* = try self.normalize(wip.scalar_ty_ref, value_id, info);
}
 
return try wip.finalize();
@@ -2599,24 +2590,22 @@ const DeclGen = struct {
defer wip_result.deinit();
var wip_ov = try self.elementWise(ov_ty);
defer wip_ov.deinit();
for (wip_result.results, wip_ov.results, 0..) |*value_id, *ov_id, i| {
for (wip_result.results, wip_ov.results, 0..) |*result_id, *ov_id, i| {
const lhs_elem_id = try wip_result.elementAt(operand_ty, lhs, i);
const rhs_elem_id = try wip_result.elementAt(operand_ty, rhs, i);
 
// Normalize both so that we can properly check for overflow
const lhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, lhs_elem_id, info);
const rhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, rhs_elem_id, info);
const op_result_id = self.spv.allocId();
const value_id = self.spv.allocId();
 
try self.func.body.emit(self.spv.gpa, add, .{
.id_result_type = wip_result.scalar_ty_id,
.id_result = op_result_id,
.operand_1 = lhs_norm_id,
.operand_2 = rhs_norm_id,
.id_result = value_id,
.operand_1 = lhs_elem_id,
.operand_2 = rhs_elem_id,
});
 
// Normalize the result so that the comparisons go well
value_id.* = try self.normalizeInt(wip_result.scalar_ty_ref, op_result_id, info);
result_id.* = try self.normalize(wip_result.scalar_ty_ref, value_id, info);
 
const overflowed_id = switch (info.signedness) {
.unsigned => blk: {
@@ -2626,8 +2615,8 @@ const DeclGen = struct {
try self.func.body.emit(self.spv.gpa, ucmp, .{
.id_result_type = self.typeId(bool_ty_ref),
.id_result = overflowed_id,
.operand_1 = value_id.*,
.operand_2 = lhs_norm_id,
.operand_1 = result_id.*,
.operand_2 = lhs_elem_id,
});
break :blk overflowed_id;
},
@@ -2654,7 +2643,7 @@ const DeclGen = struct {
try self.func.body.emit(self.spv.gpa, .OpSLessThan, .{
.id_result_type = self.typeId(bool_ty_ref),
.id_result = rhs_lt_zero_id,
.operand_1 = rhs_norm_id,
.operand_1 = rhs_elem_id,
.operand_2 = zero_id,
});
 
@@ -2662,8 +2651,8 @@ const DeclGen = struct {
try self.func.body.emit(self.spv.gpa, scmp, .{
.id_result_type = self.typeId(bool_ty_ref),
.id_result = value_gt_lhs_id,
.operand_1 = lhs_norm_id,
.operand_2 = value_id.*,
.operand_1 = lhs_elem_id,
.operand_2 = result_id.*,
});
 
const overflowed_id = self.spv.allocId();
@@ -2715,13 +2704,10 @@ const DeclGen = struct {
defer wip_result.deinit();
var wip_ov = try self.elementWise(ov_ty);
defer wip_ov.deinit();
for (0..wip_result.results.len, wip_ov.results) |i, *ov_id| {
for (wip_result.results, wip_ov.results, 0..) |*result_id, *ov_id, i| {
const lhs_elem_id = try wip_result.elementAt(operand_ty, lhs, i);
const rhs_elem_id = try wip_result.elementAt(shift_ty, rhs, i);
 
// Normalize both so that we can shift back and check if the result is the same.
const lhs_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, lhs_elem_id, info);
 
// Sometimes Zig doesn't make both of the arguments the same types here. SPIR-V expects that,
// so just manually upcast it if required.
const shift_id = if (scalar_shift_ty_ref != wip_result.scalar_ty_ref) blk: {
@@ -2733,29 +2719,41 @@ const DeclGen = struct {
});
break :blk shift_id;
} else rhs_elem_id;
const shift_norm_id = try self.normalizeInt(wip_result.scalar_ty_ref, shift_id, info);
 
const value_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftLeftLogical, .{
.id_result_type = wip_result.scalar_ty_id,
.id_result = wip_result.allocId(i),
.base = lhs_norm_id,
.shift = shift_norm_id,
.id_result = value_id,
.base = lhs_elem_id,
.shift = shift_id,
});
result_id.* = try self.normalize(wip_result.scalar_ty_ref, value_id, info);
 
// To check if overflow happened, just check if the right-shifted result is the same value.
const right_shift_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpShiftRightLogical, .{
.id_result_type = wip_result.scalar_ty_id,
.id_result = right_shift_id,
.base = try self.normalizeInt(wip_result.scalar_ty_ref, wip_result.results[i], info),
.shift = shift_norm_id,
});
switch (info.signedness) {
.signed => {
try self.func.body.emit(self.spv.gpa, .OpShiftRightArithmetic, .{
.id_result_type = wip_result.scalar_ty_id,
.id_result = right_shift_id,
.base = result_id.*,
.shift = shift_id,
});
},
.unsigned => {
try self.func.body.emit(self.spv.gpa, .OpShiftRightLogical, .{
.id_result_type = wip_result.scalar_ty_id,
.id_result = right_shift_id,
.base = result_id.*,
.shift = shift_id,
});
},
}
 
const overflowed_id = self.spv.allocId();
try self.func.body.emit(self.spv.gpa, .OpINotEqual, .{
.id_result_type = self.typeId(bool_ty_ref),
.id_result = overflowed_id,
.operand_1 = lhs_norm_id,
.operand_1 = lhs_elem_id,
.operand_2 = right_shift_id,
});
 
@@ -3113,14 +3111,7 @@ const DeclGen = struct {
.neq => .OpLogicalNotEqual,
else => unreachable,
},
.strange_integer => sign: {
const op_ty_ref = try self.resolveType(op_ty, .direct);
// Mask operands before performing comparison.
cmp_lhs_id = try self.normalizeInt(op_ty_ref, cmp_lhs_id, info);
cmp_rhs_id = try self.normalizeInt(op_ty_ref, cmp_rhs_id, info);
break :sign info.signedness;
},
.integer => info.signedness,
.integer, .strange_integer => info.signedness,
};
 
break :opcode switch (signedness) {
@@ -3252,18 +3243,13 @@ const DeclGen = struct {
const operand_id = try self.resolve(ty_op.operand);
const src_ty = self.typeOf(ty_op.operand);
const dst_ty = self.typeOfIndex(inst);
const src_ty_ref = try self.resolveType(src_ty, .direct);
const dst_ty_ref = try self.resolveType(dst_ty, .direct);
 
const src_info = try self.arithmeticTypeInfo(src_ty);
const dst_info = try self.arithmeticTypeInfo(dst_ty);
 
// While intcast promises that the value already fits, the upper bits of a
// strange integer may contain garbage. Therefore, mask/sign extend it before.
const src_id = try self.normalizeInt(src_ty_ref, operand_id, src_info);
 
if (src_info.backing_bits == dst_info.backing_bits) {
return src_id;
return operand_id;
}
 
const result_id = self.spv.allocId();
@@ -3271,14 +3257,23 @@ const DeclGen = struct {
.signed => try self.func.body.emit(self.spv.gpa, .OpSConvert, .{
.id_result_type = self.typeId(dst_ty_ref),
.id_result = result_id,
.signed_value = src_id,
.signed_value = operand_id,
}),
.unsigned => try self.func.body.emit(self.spv.gpa, .OpUConvert, .{
.id_result_type = self.typeId(dst_ty_ref),
.id_result = result_id,
.unsigned_value = src_id,
.unsigned_value = operand_id,
}),
}
 
// Make sure to normalize the result if shrinking.
// Because strange ints are sign extended in their backing
// type, we don't need to normalize when growing the type. The
// representation is already the same.
if (dst_info.bits < src_info.bits) {
return try self.normalize(dst_ty_ref, result_id, dst_info);
}
 
return result_id;
}