Skip to content

Commit 457c94d

Browse files
committed
compiler: implement @branchHint, replacing @setCold
Implements the accepted proposal to introduce `@branchHint`. This builtin is permitted as the first statement of a block if that block is the direct body of any of the following: * a function (*not* a `test`) * either branch of an `if` * the RHS of a `catch` or `orelse` * a `switch` prong * an `or` or `and` expression It lowers to the ZIR instruction `extended(branch_hint(...))`. When Sema encounters this instruction, it sets `sema.branch_hint` appropriately, and `zirCondBr` etc are expected to reset this value as necessary. The state is on `Sema` rather than `Block` to make it automatically propagate up non-conditional blocks without special handling. If `@panic` is reached, the branch hint is set to `.cold` if none was already set; similarly, error branches get a hint of `.unlikely` if no hint is explicitly provided. If a condition is comptime-known, `cold` hints from the taken branch are allowed to propagate up, but other hints are discarded. This is because a `likely`/`unlikely` hint just indicates the direction this branch is likely to go, which is redundant information when the branch is known at comptime; but `cold` hints indicate that control flow is unlikely to ever reach this branch, meaning if the branch is always taken from its parent, then the parent is also unlikely to ever be reached. This branch information is stored in AIR `cond_br` and `switch_br`. In addition, `try` and `try_ptr` instructions have variants `try_cold` and `try_ptr_cold` which indicate that the error case is cold (rather than just unlikely); this is reachable through e.g. `errdefer unreachable` or `errdefer @Panic("")`. A new API `unwrapSwitch` is introduced to `Air` to make it more convenient to access `switch_br` instructions. In time, I plan to update all AIR instructions to be accessed via an `unwrap` method which returns a convenient tagged union a la `InternPool.indexToKey`. The LLVM backend lowers branch hints for conditional branches and switches as follows: * If any branch is marked `unpredictable`, the instruction is marked `!unpredictable`. * Any branch which is marked as `cold` gets a `llvm.assume(i1 true) [ "cold"() ]` call to mark the code path cold. * If any branch is marked `likely` or `unlikely`, branch weight metadata is attached with `!prof`. Likely branches get a weight of 2000, and unlikely branches a weight of 1. In `switch` statements, un-annotated branches get a weight of 1000 as a "middle ground" hint, since there could be likely *and* unlikely *and* un-annotated branches. For functions, a `cold` hint corresponds to the `cold` function attribute, and other hints are currently ignored -- as far as I can tell LLVM doesn't really have a way to lower them. (Ideally, we would want the branch hint given in the function to propagate to call sites.) The compiler and standard library do not yet use this new builtin. Resolves: #21148
1 parent 72e0080 commit 457c94d

25 files changed

+1127
-563
lines changed

lib/std/builtin.zig

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,25 @@ pub const ExternOptions = struct {
675675
is_thread_local: bool = false,
676676
};
677677

678+
/// This data structure is used by the Zig language code generation and
679+
/// therefore must be kept in sync with the compiler implementation.
680+
pub const BranchHint = enum(u3) {
681+
/// Equivalent to no hint given.
682+
none,
683+
/// This branch of control flow is more likely to be reached than its peers.
684+
/// The optimizer should optimize for reaching it.
685+
likely,
686+
/// This branch of control flow is less likely to be reached than its peers.
687+
/// The optimizer should optimize for not reaching it.
688+
unlikely,
689+
/// This branch of control flow is unlikely to *ever* be reached.
690+
/// The optimizer may place it in a different page of memory to optimize other branches.
691+
cold,
692+
/// It is difficult to predict whether this branch of control flow will be reached.
693+
/// The optimizer should avoid branching behavior with expensive mispredictions.
694+
unpredictable,
695+
};
696+
678697
/// This enum is set by the compiler and communicates which compiler backend is
679698
/// used to produce machine code.
680699
/// Think carefully before deciding to observe this value. Nearly all code should

lib/std/zig/AstGen.zig

Lines changed: 91 additions & 53 deletions
Large diffs are not rendered by default.

lib/std/zig/AstRlAnnotate.zig

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,10 @@ fn builtinCall(astrl: *AstRlAnnotate, block: ?*Block, ri: ResultInfo, node: Ast.
829829
}
830830
switch (info.tag) {
831831
.import => return false,
832+
.branch_hint => {
833+
_ = try astrl.expr(args[0], block, ResultInfo.type_only);
834+
return false;
835+
},
832836
.compile_log, .TypeOf => {
833837
for (args) |arg_node| {
834838
_ = try astrl.expr(arg_node, block, ResultInfo.none);
@@ -907,7 +911,6 @@ fn builtinCall(astrl: *AstRlAnnotate, block: ?*Block, ri: ResultInfo, node: Ast.
907911
.fence,
908912
.set_float_mode,
909913
.set_align_stack,
910-
.set_cold,
911914
.type_info,
912915
.work_item_id,
913916
.work_group_size,

lib/std/zig/BuiltinFn.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub const Tag = enum {
1414
bit_offset_of,
1515
int_from_bool,
1616
bit_size_of,
17+
branch_hint,
1718
breakpoint,
1819
disable_instrumentation,
1920
mul_add,
@@ -82,7 +83,6 @@ pub const Tag = enum {
8283
return_address,
8384
select,
8485
set_align_stack,
85-
set_cold,
8686
set_eval_branch_quota,
8787
set_float_mode,
8888
set_runtime_safety,
@@ -256,6 +256,14 @@ pub const list = list: {
256256
.param_count = 1,
257257
},
258258
},
259+
.{
260+
"@branchHint",
261+
.{
262+
.tag = .branch_hint,
263+
.param_count = 1,
264+
.illegal_outside_function = true,
265+
},
266+
},
259267
.{
260268
"@breakpoint",
261269
.{
@@ -744,14 +752,6 @@ pub const list = list: {
744752
.illegal_outside_function = true,
745753
},
746754
},
747-
.{
748-
"@setCold",
749-
.{
750-
.tag = .set_cold,
751-
.param_count = 1,
752-
.illegal_outside_function = true,
753-
},
754-
},
755755
.{
756756
"@setEvalBranchQuota",
757757
.{

lib/std/zig/Zir.zig

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,7 +1546,7 @@ pub const Inst = struct {
15461546
=> false,
15471547

15481548
.extended => switch (data.extended.opcode) {
1549-
.fence, .set_cold, .breakpoint, .disable_instrumentation => true,
1549+
.fence, .branch_hint, .breakpoint, .disable_instrumentation => true,
15501550
else => false,
15511551
},
15521552
};
@@ -1954,9 +1954,6 @@ pub const Inst = struct {
19541954
/// Implement builtin `@setAlignStack`.
19551955
/// `operand` is payload index to `UnNode`.
19561956
set_align_stack,
1957-
/// Implements `@setCold`.
1958-
/// `operand` is payload index to `UnNode`.
1959-
set_cold,
19601957
/// Implements the `@errorCast` builtin.
19611958
/// `operand` is payload index to `BinNode`. `lhs` is dest type, `rhs` is operand.
19621959
error_cast,
@@ -2051,6 +2048,10 @@ pub const Inst = struct {
20512048
/// `operand` is `src_node: i32`.
20522049
/// `small` is an `Inst.BuiltinValue`.
20532050
builtin_value,
2051+
/// Provide a `@branchHint` for the current block.
2052+
/// `operand` is payload index to `UnNode`.
2053+
/// `small` is unused.
2054+
branch_hint,
20542055

20552056
pub const InstData = struct {
20562057
opcode: Extended,
@@ -3142,6 +3143,7 @@ pub const Inst = struct {
31423143
export_options,
31433144
extern_options,
31443145
type_info,
3146+
branch_hint,
31453147
// Values
31463148
calling_convention_c,
31473149
calling_convention_inline,
@@ -3962,7 +3964,6 @@ fn findDeclsInner(
39623964
.fence,
39633965
.set_float_mode,
39643966
.set_align_stack,
3965-
.set_cold,
39663967
.error_cast,
39673968
.await_nosuspend,
39683969
.breakpoint,
@@ -3986,6 +3987,7 @@ fn findDeclsInner(
39863987
.closure_get,
39873988
.field_parent_ptr,
39883989
.builtin_value,
3990+
.branch_hint,
39893991
=> return,
39903992

39913993
// `@TypeOf` has a body.

src/Air.zig

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,13 +433,18 @@ pub const Inst = struct {
433433
/// In the case of non-error, control flow proceeds to the next instruction
434434
/// after the `try`, with the result of this instruction being the unwrapped
435435
/// payload value, as if `unwrap_errunion_payload` was executed on the operand.
436+
/// The error branch is considered to have a branch hint of `.unlikely`.
436437
/// Uses the `pl_op` field. Payload is `Try`.
437438
@"try",
439+
/// Same as `try` except the error branch hint is `.cold`.
440+
try_cold,
438441
/// Same as `try` except the operand is a pointer to an error union, and the
439442
/// result is a pointer to the payload. Result is as if `unwrap_errunion_payload_ptr`
440443
/// was executed on the operand.
441444
/// Uses the `ty_pl` field. Payload is `TryPtr`.
442445
try_ptr,
446+
/// Same as `try_ptr` except the error branch hint is `.cold`.
447+
try_ptr_cold,
443448
/// Notes the beginning of a source code statement and marks the line and column.
444449
/// Result type is always void.
445450
/// Uses the `dbg_stmt` field.
@@ -1116,11 +1121,20 @@ pub const Call = struct {
11161121
pub const CondBr = struct {
11171122
then_body_len: u32,
11181123
else_body_len: u32,
1124+
branch_hints: BranchHints,
1125+
pub const BranchHints = packed struct(u32) {
1126+
true: std.builtin.BranchHint,
1127+
false: std.builtin.BranchHint,
1128+
_: u26 = 0,
1129+
};
11191130
};
11201131

11211132
/// Trailing:
1122-
/// * 0. `Case` for each `cases_len`
1123-
/// * 1. the else body, according to `else_body_len`.
1133+
/// * 0. `BranchHint` for each `cases_len + 1`. bit-packed into `u32`
1134+
/// elems such that each `u32` contains up to 10x `BranchHint`.
1135+
/// LSBs are first case. Final hint is `else`.
1136+
/// * 1. `Case` for each `cases_len`
1137+
/// * 2. the else body, according to `else_body_len`.
11241138
pub const SwitchBr = struct {
11251139
cases_len: u32,
11261140
else_body_len: u32,
@@ -1380,6 +1394,7 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
13801394
.ptr_add,
13811395
.ptr_sub,
13821396
.try_ptr,
1397+
.try_ptr_cold,
13831398
=> return datas[@intFromEnum(inst)].ty_pl.ty.toType(),
13841399

13851400
.not,
@@ -1500,7 +1515,7 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
15001515
return air.typeOf(extra.lhs, ip);
15011516
},
15021517

1503-
.@"try" => {
1518+
.@"try", .try_cold => {
15041519
const err_union_ty = air.typeOf(datas[@intFromEnum(inst)].pl_op.operand, ip);
15051520
return Type.fromInterned(ip.indexToKey(err_union_ty.ip_index).error_union_type.payload_type);
15061521
},
@@ -1524,9 +1539,8 @@ pub fn extraData(air: Air, comptime T: type, index: usize) struct { data: T, end
15241539
inline for (fields) |field| {
15251540
@field(result, field.name) = switch (field.type) {
15261541
u32 => air.extra[i],
1527-
Inst.Ref => @as(Inst.Ref, @enumFromInt(air.extra[i])),
1528-
i32 => @as(i32, @bitCast(air.extra[i])),
1529-
InternPool.Index => @as(InternPool.Index, @enumFromInt(air.extra[i])),
1542+
InternPool.Index, Inst.Ref => @enumFromInt(air.extra[i]),
1543+
i32, CondBr.BranchHints => @bitCast(air.extra[i]),
15301544
else => @compileError("bad field type: " ++ @typeName(field.type)),
15311545
};
15321546
i += 1;
@@ -1593,7 +1607,9 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
15931607
.cond_br,
15941608
.switch_br,
15951609
.@"try",
1610+
.try_cold,
15961611
.try_ptr,
1612+
.try_ptr_cold,
15971613
.dbg_stmt,
15981614
.dbg_inline_block,
15991615
.dbg_var_ptr,
@@ -1796,4 +1812,91 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
17961812
};
17971813
}
17981814

1815+
pub const UnwrappedSwitch = struct {
1816+
air: *const Air,
1817+
operand: Inst.Ref,
1818+
cases_len: u32,
1819+
else_body_len: u32,
1820+
branch_hints_start: u32,
1821+
cases_start: u32,
1822+
1823+
/// Asserts that `case_idx < us.cases_len`.
1824+
pub fn getHint(us: UnwrappedSwitch, case_idx: u32) std.builtin.BranchHint {
1825+
assert(case_idx < us.cases_len);
1826+
return us.getHintInner(case_idx);
1827+
}
1828+
pub fn getElseHint(us: UnwrappedSwitch) std.builtin.BranchHint {
1829+
return us.getHintInner(us.cases_len);
1830+
}
1831+
fn getHintInner(us: UnwrappedSwitch, idx: u32) std.builtin.BranchHint {
1832+
const bag = us.air.extra[us.branch_hints_start..][idx / 10];
1833+
const bits: u3 = @truncate(bag >> @intCast(3 * (idx % 10)));
1834+
return @enumFromInt(bits);
1835+
}
1836+
1837+
pub fn iterateCases(us: UnwrappedSwitch) CaseIterator {
1838+
return .{
1839+
.air = us.air,
1840+
.cases_len = us.cases_len,
1841+
.else_body_len = us.else_body_len,
1842+
.next_case = 0,
1843+
.extra_index = us.cases_start,
1844+
};
1845+
}
1846+
pub const CaseIterator = struct {
1847+
air: *const Air,
1848+
cases_len: u32,
1849+
else_body_len: u32,
1850+
next_case: u32,
1851+
extra_index: u32,
1852+
1853+
pub fn next(it: *CaseIterator) ?Case {
1854+
if (it.next_case == it.cases_len) return null;
1855+
const idx = it.next_case;
1856+
it.next_case += 1;
1857+
1858+
const extra = it.air.extraData(SwitchBr.Case, it.extra_index);
1859+
var extra_index = extra.end;
1860+
const items: []const Inst.Ref = @ptrCast(it.air.extra[extra_index..][0..extra.data.items_len]);
1861+
extra_index += items.len;
1862+
const body: []const Inst.Index = @ptrCast(it.air.extra[extra_index..][0..extra.data.body_len]);
1863+
extra_index += body.len;
1864+
it.extra_index = @intCast(extra_index);
1865+
1866+
return .{
1867+
.idx = idx,
1868+
.items = items,
1869+
.body = body,
1870+
};
1871+
}
1872+
/// Only valid to call once all cases have been iterated, i.e. `next` returns `null`.
1873+
/// Returns the body of the "default" (`else`) case.
1874+
pub fn elseBody(it: *CaseIterator) []const Inst.Index {
1875+
assert(it.next_case == it.cases_len);
1876+
return @ptrCast(it.air.extra[it.extra_index..][0..it.else_body_len]);
1877+
}
1878+
pub const Case = struct {
1879+
idx: u32,
1880+
items: []const Inst.Ref,
1881+
body: []const Inst.Index,
1882+
};
1883+
};
1884+
};
1885+
1886+
pub fn unwrapSwitch(air: *const Air, switch_inst: Inst.Index) UnwrappedSwitch {
1887+
const inst = air.instructions.get(@intFromEnum(switch_inst));
1888+
assert(inst.tag == .switch_br);
1889+
const pl_op = inst.data.pl_op;
1890+
const extra = air.extraData(SwitchBr, pl_op.payload);
1891+
const hint_bag_count = std.math.divCeil(usize, extra.data.cases_len + 1, 10) catch unreachable;
1892+
return .{
1893+
.air = air,
1894+
.operand = pl_op.operand,
1895+
.cases_len = extra.data.cases_len,
1896+
.else_body_len = extra.data.else_body_len,
1897+
.branch_hints_start = @intCast(extra.end),
1898+
.cases_start = @intCast(extra.end + hint_bag_count),
1899+
};
1900+
}
1901+
17991902
pub const typesFullyResolved = @import("Air/types_resolved.zig").typesFullyResolved;

src/Air/types_resolved.zig

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
344344
if (!checkRef(data.pl_op.operand, zcu)) return false;
345345
},
346346

347-
.@"try" => {
347+
.@"try", .try_cold => {
348348
const extra = air.extraData(Air.Try, data.pl_op.payload);
349349
if (!checkRef(data.pl_op.operand, zcu)) return false;
350350
if (!checkBody(
@@ -354,7 +354,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
354354
)) return false;
355355
},
356356

357-
.try_ptr => {
357+
.try_ptr, .try_ptr_cold => {
358358
const extra = air.extraData(Air.TryPtr, data.ty_pl.payload);
359359
if (!checkType(data.ty_pl.ty.toType(), zcu)) return false;
360360
if (!checkRef(extra.data.ptr, zcu)) return false;
@@ -381,27 +381,14 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
381381
},
382382

383383
.switch_br => {
384-
const extra = air.extraData(Air.SwitchBr, data.pl_op.payload);
385-
if (!checkRef(data.pl_op.operand, zcu)) return false;
386-
var extra_index = extra.end;
387-
for (0..extra.data.cases_len) |_| {
388-
const case = air.extraData(Air.SwitchBr.Case, extra_index);
389-
extra_index = case.end;
390-
const items: []const Air.Inst.Ref = @ptrCast(air.extra[extra_index..][0..case.data.items_len]);
391-
extra_index += case.data.items_len;
392-
for (items) |item| if (!checkRef(item, zcu)) return false;
393-
if (!checkBody(
394-
air,
395-
@ptrCast(air.extra[extra_index..][0..case.data.body_len]),
396-
zcu,
397-
)) return false;
398-
extra_index += case.data.body_len;
384+
const switch_br = air.unwrapSwitch(inst);
385+
if (!checkRef(switch_br.operand, zcu)) return false;
386+
var it = switch_br.iterateCases();
387+
while (it.next()) |case| {
388+
for (case.items) |item| if (!checkRef(item, zcu)) return false;
389+
if (!checkBody(air, case.body, zcu)) return false;
399390
}
400-
if (!checkBody(
401-
air,
402-
@ptrCast(air.extra[extra_index..][0..extra.data.else_body_len]),
403-
zcu,
404-
)) return false;
391+
if (!checkBody(air, it.elseBody(), zcu)) return false;
405392
},
406393

407394
.assembly => {

0 commit comments

Comments
 (0)