Skip to content

introduce labeled continue syntax inside a switch expression #8220

Description

@andrewrk

Background

The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

Problem Statement

For example (godbolt link):

constInst=externstruct {
tag: Tag,
constTag=externenum {
add,
addwrap,
alloc,
alloc_mut,
alloc_inferred,
alloc_inferred_mut,
anyframe_type,
array_cat,
array_mul,
array_type,
array_type_sentinel,
indexable_ptr_len,
};
};
exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
constinst_list=inst_list_ptr[0..inst_list_len];
for (inst_list) |inst, i| {
map[i] =switch (inst.tag) {
.add=>analyze_add(inst),
.addwrap=>analyze_addwrap(inst),
.alloc=>analyze_alloc(inst),
.alloc_mut=>analyze_alloc_mut(inst),
.alloc_inferred=>analyze_alloc_inferred(inst),
.alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
.anyframe_type=>analyze_anyframe_type(inst),
.array_cat=>analyze_array_cat(inst),
.array_mul=>analyze_array_mul(inst),
.array_type=>analyze_array_type(inst),
.array_type_sentinel=>analyze_array_type_sentinel(inst),
.indexable_ptr_len=>analyze_indexable_ptr_len(inst),
};
}
}
externfnanalyze_add(inst: Inst) u32;
externfnanalyze_addwrap(inst: Inst) u32;
externfnanalyze_alloc(inst: Inst) u32;
externfnanalyze_alloc_mut(inst: Inst) u32;
externfnanalyze_alloc_inferred(inst: Inst) u32;
externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
externfnanalyze_anyframe_type(inst: Inst) u32;
externfnanalyze_array_cat(inst: Inst) u32;
externfnanalyze_array_mul(inst: Inst) u32;
externfnanalyze_array_type(inst: Inst) u32;
externfnanalyze_array_type_sentinel(inst: Inst) u32;
externfnanalyze_indexable_ptr_len(inst: Inst) u32;

In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

.LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

They explain it in a nice, intuitive way here:

Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

Research Dump

Can LLVM Do the Optimization?

In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

constInst=externstruct {
tag: Tag,
constTag=externenum {
add,
addwrap,
alloc,
alloc_mut,
alloc_inferred,
alloc_inferred_mut,
anyframe_type,
array_cat,
array_mul,
};
};
exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
constinst_list=inst_list_ptr[0..inst_list_len];
vari: usize=0;
while (true) {
constinst=inst_list[i];
switch (inst.tag) {
.add=> {
map[i] =analyze_add(inst);
i+=1;
if (i<inst_list_len) continue;
},
.addwrap=> {
map[i] =analyze_addwrap(inst);
i+=1;
if (i<inst_list_len) continue;
},
.alloc=> {
map[i] =analyze_alloc(inst);
i+=1;
if (i<inst_list_len) continue;
},
.alloc_mut=> {
map[i] =analyze_alloc_mut(inst);
i+=1;
if (i<inst_list_len) continue;
},
.alloc_inferred=> {
map[i] =analyze_alloc_inferred(inst);
i+=1;
if (i<inst_list_len) continue;
},
.alloc_inferred_mut=> {
map[i] =analyze_alloc_inferred_mut(inst);
i+=1;
if (i<inst_list_len) continue;
},
.anyframe_type=> {
map[i] =analyze_anyframe_type(inst);
i+=1;
if (i<inst_list_len) continue;
},
.array_cat=> {
map[i] =analyze_array_cat(inst);
i+=1;
if (i<inst_list_len) continue;
},
.array_mul=> {
map[i] =analyze_array_mul(inst);
i+=1;
if (i<inst_list_len) continue;
},
}
break;
}
}
externfnanalyze_add(inst: Inst) u32;
externfnanalyze_addwrap(inst: Inst) u32;
externfnanalyze_alloc(inst: Inst) u32;
externfnanalyze_alloc_mut(inst: Inst) u32;
externfnanalyze_alloc_inferred(inst: Inst) u32;
externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
externfnanalyze_anyframe_type(inst: Inst) u32;
externfnanalyze_array_cat(inst: Inst) u32;
externfnanalyze_array_mul(inst: Inst) u32;

Snippet of assembly:

.LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

EDIT: New Discovery

It does not get much simpler than this

Wrong!

After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

constInst=externstruct {
tag: Tag,
constTag=externenum {
end,
add,
addwrap,
alloc,
alloc_mut,
alloc_inferred,
alloc_inferred_mut,
anyframe_type,
array_cat,
array_mul,
};
};
exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
vari: usize=0;
while (true) {
constinst=inst_list[i];
switch (inst.tag) {
.end=>return,
.add=> {
map[i] =analyze_add(inst);
i+=1;
continue;
},
.addwrap=> {
map[i] =analyze_addwrap(inst);
i+=1;
continue;
},
.alloc=> {
map[i] =analyze_alloc(inst);
i+=1;
continue;
},
.alloc_mut=> {
map[i] =analyze_alloc_mut(inst);
i+=1;
continue;
},
.alloc_inferred=> {
map[i] =analyze_alloc_inferred(inst);
i+=1;
continue;
},
.alloc_inferred_mut=> {
map[i] =analyze_alloc_inferred_mut(inst);
i+=1;
continue;
},
.anyframe_type=> {
map[i] =analyze_anyframe_type(inst);
i+=1;
continue;
},
.array_cat=> {
map[i] =analyze_array_cat(inst);
i+=1;
continue;
},
.array_mul=> {
map[i] =analyze_array_mul(inst);
i+=1;
continue;
},
}
break;
}
}
externfnanalyze_add(inst: Inst) u32;
externfnanalyze_addwrap(inst: Inst) u32;
externfnanalyze_alloc(inst: Inst) u32;
externfnanalyze_alloc_mut(inst: Inst) u32;
externfnanalyze_alloc_inferred(inst: Inst) u32;
externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
externfnanalyze_anyframe_type(inst: Inst) u32;
externfnanalyze_array_cat(inst: Inst) u32;
externfnanalyze_array_mul(inst: Inst) u32;

Two example prongs from the machine code:

.LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

It's perfect! This is exactly what we wanted.

This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

Real Actual Use Case

Here's one in the self-hosted compiler:

switch (old_inst.tag) {

This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

This pattern also exists in:

  • The tokenizer
  • The parser
  • astgen
  • sema (this is the linked one above)
  • codegen
  • translate-c
  • zig fmt

(pretty much in every stage of the pipeline)

Other Possible Solution: Tail Calls

Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

This is reasonable in the sense that it is doable right now; however there are some problems:

  • As far as I understand, tail calls don't work on some architectures.
    • (what are these? does anybody know?)
  • I'm also concerned about trying to debug when doing dispatch with tail calls.
  • It forces you to organize your logic into functions. That's another jump that
    maybe you did not want in your hot path.

Proposal

I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

constInst=externstruct {
tag: Tag,
constTag=externenum {
end,
add,
addwrap,
alloc,
alloc_mut,
alloc_inferred,
alloc_inferred_mut,
anyframe_type,
array_cat,
array_mul,
};
};
exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
vari: usize=0;
sw: switch (inst_list[i].tag) {
.end=>return,
.add=> {
map[i] =analyze_add(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.addwrap=> {
map[i] =analyze_addwrap(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.alloc=> {
map[i] =analyze_alloc(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.alloc_mut=> {
map[i] =analyze_alloc_mut(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.alloc_inferred=> {
map[i] =analyze_alloc_inferred(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.alloc_inferred_mut=> {
map[i] =analyze_alloc_inferred_mut(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.anyframe_type=> {
map[i] =analyze_anyframe_type(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.array_cat=> {
map[i] =analyze_array_cat(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
.array_mul=> {
map[i] =analyze_array_mul(inst_list[i]);
i+=1;
continue :swinst_list[i].tag;
},
}
}
externfnanalyze_add(inst: Inst) u32;
externfnanalyze_addwrap(inst: Inst) u32;
externfnanalyze_alloc(inst: Inst) u32;
externfnanalyze_alloc_mut(inst: Inst) u32;
externfnanalyze_alloc_inferred(inst: Inst) u32;
externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
externfnanalyze_anyframe_type(inst: Inst) u32;
externfnanalyze_array_cat(inst: Inst) u32;
externfnanalyze_array_mul(inst: Inst) u32;

The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

  • labeled continue with an operand on a loop would be a compile error
  • labeled break with a switch would be OK.

How to Lower this to LLVM

Note: I wrote this section before the EDIT: New Discovery section.

One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
]
SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
]

The machine code for the prongs looks like this:

<snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

.LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

How to Lower this in Self-Hosted Backends

We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

OK But Is The Perf Actually Good?

I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
    Skip to content

    introduce labeled continue syntax inside a switch expression #8220

    Description

    @andrewrk

    Background

    The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

    However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

    Problem Statement

    For example (godbolt link):

    constInst=externstruct {
    tag: Tag,
    constTag=externenum {
    add,
    addwrap,
    alloc,
    alloc_mut,
    alloc_inferred,
    alloc_inferred_mut,
    anyframe_type,
    array_cat,
    array_mul,
    array_type,
    array_type_sentinel,
    indexable_ptr_len,
    };
    };
    exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
    constinst_list=inst_list_ptr[0..inst_list_len];
    for (inst_list) |inst, i| {
    map[i] =switch (inst.tag) {
    .add=>analyze_add(inst),
    .addwrap=>analyze_addwrap(inst),
    .alloc=>analyze_alloc(inst),
    .alloc_mut=>analyze_alloc_mut(inst),
    .alloc_inferred=>analyze_alloc_inferred(inst),
    .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
    .anyframe_type=>analyze_anyframe_type(inst),
    .array_cat=>analyze_array_cat(inst),
    .array_mul=>analyze_array_mul(inst),
    .array_type=>analyze_array_type(inst),
    .array_type_sentinel=>analyze_array_type_sentinel(inst),
    .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
    };
    }
    }
    externfnanalyze_add(inst: Inst) u32;
    externfnanalyze_addwrap(inst: Inst) u32;
    externfnanalyze_alloc(inst: Inst) u32;
    externfnanalyze_alloc_mut(inst: Inst) u32;
    externfnanalyze_alloc_inferred(inst: Inst) u32;
    externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
    externfnanalyze_anyframe_type(inst: Inst) u32;
    externfnanalyze_array_cat(inst: Inst) u32;
    externfnanalyze_array_mul(inst: Inst) u32;
    externfnanalyze_array_type(inst: Inst) u32;
    externfnanalyze_array_type_sentinel(inst: Inst) u32;
    externfnanalyze_indexable_ptr_len(inst: Inst) u32;

    In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

    .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

    The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

    Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

    This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

    They explain it in a nice, intuitive way here:

    Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

    So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

    In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

    Research Dump

    Can LLVM Do the Optimization?

    In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

    constInst=externstruct {
    tag: Tag,
    constTag=externenum {
    add,
    addwrap,
    alloc,
    alloc_mut,
    alloc_inferred,
    alloc_inferred_mut,
    anyframe_type,
    array_cat,
    array_mul,
    };
    };
    exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
    constinst_list=inst_list_ptr[0..inst_list_len];
    vari: usize=0;
    while (true) {
    constinst=inst_list[i];
    switch (inst.tag) {
    .add=> {
    map[i] =analyze_add(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .addwrap=> {
    map[i] =analyze_addwrap(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .alloc=> {
    map[i] =analyze_alloc(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .alloc_mut=> {
    map[i] =analyze_alloc_mut(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .alloc_inferred=> {
    map[i] =analyze_alloc_inferred(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .alloc_inferred_mut=> {
    map[i] =analyze_alloc_inferred_mut(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .anyframe_type=> {
    map[i] =analyze_anyframe_type(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .array_cat=> {
    map[i] =analyze_array_cat(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    .array_mul=> {
    map[i] =analyze_array_mul(inst);
    i+=1;
    if (i<inst_list_len) continue;
    },
    }
    break;
    }
    }
    externfnanalyze_add(inst: Inst) u32;
    externfnanalyze_addwrap(inst: Inst) u32;
    externfnanalyze_alloc(inst: Inst) u32;
    externfnanalyze_alloc_mut(inst: Inst) u32;
    externfnanalyze_alloc_inferred(inst: Inst) u32;
    externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
    externfnanalyze_anyframe_type(inst: Inst) u32;
    externfnanalyze_array_cat(inst: Inst) u32;
    externfnanalyze_array_mul(inst: Inst) u32;

    Snippet of assembly:

    .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

    Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

    EDIT: New Discovery

    It does not get much simpler than this

    Wrong!

    After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

    constInst=externstruct {
    tag: Tag,
    constTag=externenum {
    end,
    add,
    addwrap,
    alloc,
    alloc_mut,
    alloc_inferred,
    alloc_inferred_mut,
    anyframe_type,
    array_cat,
    array_mul,
    };
    };
    exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
    vari: usize=0;
    while (true) {
    constinst=inst_list[i];
    switch (inst.tag) {
    .end=>return,
    .add=> {
    map[i] =analyze_add(inst);
    i+=1;
    continue;
    },
    .addwrap=> {
    map[i] =analyze_addwrap(inst);
    i+=1;
    continue;
    },
    .alloc=> {
    map[i] =analyze_alloc(inst);
    i+=1;
    continue;
    },
    .alloc_mut=> {
    map[i] =analyze_alloc_mut(inst);
    i+=1;
    continue;
    },
    .alloc_inferred=> {
    map[i] =analyze_alloc_inferred(inst);
    i+=1;
    continue;
    },
    .alloc_inferred_mut=> {
    map[i] =analyze_alloc_inferred_mut(inst);
    i+=1;
    continue;
    },
    .anyframe_type=> {
    map[i] =analyze_anyframe_type(inst);
    i+=1;
    continue;
    },
    .array_cat=> {
    map[i] =analyze_array_cat(inst);
    i+=1;
    continue;
    },
    .array_mul=> {
    map[i] =analyze_array_mul(inst);
    i+=1;
    continue;
    },
    }
    break;
    }
    }
    externfnanalyze_add(inst: Inst) u32;
    externfnanalyze_addwrap(inst: Inst) u32;
    externfnanalyze_alloc(inst: Inst) u32;
    externfnanalyze_alloc_mut(inst: Inst) u32;
    externfnanalyze_alloc_inferred(inst: Inst) u32;
    externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
    externfnanalyze_anyframe_type(inst: Inst) u32;
    externfnanalyze_array_cat(inst: Inst) u32;
    externfnanalyze_array_mul(inst: Inst) u32;

    Two example prongs from the machine code:

    .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

    It's perfect! This is exactly what we wanted.

    This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

    Real Actual Use Case

    Here's one in the self-hosted compiler:

    switch (old_inst.tag) {

    This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

    This pattern also exists in:

    • The tokenizer
    • The parser
    • astgen
    • sema (this is the linked one above)
    • codegen
    • translate-c
    • zig fmt

    (pretty much in every stage of the pipeline)

    Other Possible Solution: Tail Calls

    Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

    This is reasonable in the sense that it is doable right now; however there are some problems:

    • As far as I understand, tail calls don't work on some architectures.
      • (what are these? does anybody know?)
    • I'm also concerned about trying to debug when doing dispatch with tail calls.
    • It forces you to organize your logic into functions. That's another jump that
      maybe you did not want in your hot path.

    Proposal

    I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

    constInst=externstruct {
    tag: Tag,
    constTag=externenum {
    end,
    add,
    addwrap,
    alloc,
    alloc_mut,
    alloc_inferred,
    alloc_inferred_mut,
    anyframe_type,
    array_cat,
    array_mul,
    };
    };
    exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
    vari: usize=0;
    sw: switch (inst_list[i].tag) {
    .end=>return,
    .add=> {
    map[i] =analyze_add(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .addwrap=> {
    map[i] =analyze_addwrap(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .alloc=> {
    map[i] =analyze_alloc(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .alloc_mut=> {
    map[i] =analyze_alloc_mut(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .alloc_inferred=> {
    map[i] =analyze_alloc_inferred(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .alloc_inferred_mut=> {
    map[i] =analyze_alloc_inferred_mut(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .anyframe_type=> {
    map[i] =analyze_anyframe_type(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .array_cat=> {
    map[i] =analyze_array_cat(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    .array_mul=> {
    map[i] =analyze_array_mul(inst_list[i]);
    i+=1;
    continue :swinst_list[i].tag;
    },
    }
    }
    externfnanalyze_add(inst: Inst) u32;
    externfnanalyze_addwrap(inst: Inst) u32;
    externfnanalyze_alloc(inst: Inst) u32;
    externfnanalyze_alloc_mut(inst: Inst) u32;
    externfnanalyze_alloc_inferred(inst: Inst) u32;
    externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
    externfnanalyze_anyframe_type(inst: Inst) u32;
    externfnanalyze_array_cat(inst: Inst) u32;
    externfnanalyze_array_mul(inst: Inst) u32;

    The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

    • labeled continue with an operand on a loop would be a compile error
    • labeled break with a switch would be OK.

    How to Lower this to LLVM

    Note: I wrote this section before the EDIT: New Discovery section.

    One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

    SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
    storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
    i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
    ]
    SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
    storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
    i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
    ]

    The machine code for the prongs looks like this:

    <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

    Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

    .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

    The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

    Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

    How to Lower this in Self-Hosted Backends

    We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

    OK But Is The Perf Actually Good?

    I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

      Type

      No type

      Projects

      No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
      Skip to content

      introduce labeled continue syntax inside a switch expression #8220

      Description

      @andrewrk

      Background

      The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

      However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

      Problem Statement

      For example (godbolt link):

      constInst=externstruct {
      tag: Tag,
      constTag=externenum {
      add,
      addwrap,
      alloc,
      alloc_mut,
      alloc_inferred,
      alloc_inferred_mut,
      anyframe_type,
      array_cat,
      array_mul,
      array_type,
      array_type_sentinel,
      indexable_ptr_len,
      };
      };
      exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
      constinst_list=inst_list_ptr[0..inst_list_len];
      for (inst_list) |inst, i| {
      map[i] =switch (inst.tag) {
      .add=>analyze_add(inst),
      .addwrap=>analyze_addwrap(inst),
      .alloc=>analyze_alloc(inst),
      .alloc_mut=>analyze_alloc_mut(inst),
      .alloc_inferred=>analyze_alloc_inferred(inst),
      .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
      .anyframe_type=>analyze_anyframe_type(inst),
      .array_cat=>analyze_array_cat(inst),
      .array_mul=>analyze_array_mul(inst),
      .array_type=>analyze_array_type(inst),
      .array_type_sentinel=>analyze_array_type_sentinel(inst),
      .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
      };
      }
      }
      externfnanalyze_add(inst: Inst) u32;
      externfnanalyze_addwrap(inst: Inst) u32;
      externfnanalyze_alloc(inst: Inst) u32;
      externfnanalyze_alloc_mut(inst: Inst) u32;
      externfnanalyze_alloc_inferred(inst: Inst) u32;
      externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
      externfnanalyze_anyframe_type(inst: Inst) u32;
      externfnanalyze_array_cat(inst: Inst) u32;
      externfnanalyze_array_mul(inst: Inst) u32;
      externfnanalyze_array_type(inst: Inst) u32;
      externfnanalyze_array_type_sentinel(inst: Inst) u32;
      externfnanalyze_indexable_ptr_len(inst: Inst) u32;

      In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

      .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

      The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

      Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

      This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

      They explain it in a nice, intuitive way here:

      Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

      So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

      In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

      Research Dump

      Can LLVM Do the Optimization?

      In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

      constInst=externstruct {
      tag: Tag,
      constTag=externenum {
      add,
      addwrap,
      alloc,
      alloc_mut,
      alloc_inferred,
      alloc_inferred_mut,
      anyframe_type,
      array_cat,
      array_mul,
      };
      };
      exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
      constinst_list=inst_list_ptr[0..inst_list_len];
      vari: usize=0;
      while (true) {
      constinst=inst_list[i];
      switch (inst.tag) {
      .add=> {
      map[i] =analyze_add(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .addwrap=> {
      map[i] =analyze_addwrap(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .alloc=> {
      map[i] =analyze_alloc(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .alloc_mut=> {
      map[i] =analyze_alloc_mut(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .alloc_inferred=> {
      map[i] =analyze_alloc_inferred(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .alloc_inferred_mut=> {
      map[i] =analyze_alloc_inferred_mut(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .anyframe_type=> {
      map[i] =analyze_anyframe_type(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .array_cat=> {
      map[i] =analyze_array_cat(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      .array_mul=> {
      map[i] =analyze_array_mul(inst);
      i+=1;
      if (i<inst_list_len) continue;
      },
      }
      break;
      }
      }
      externfnanalyze_add(inst: Inst) u32;
      externfnanalyze_addwrap(inst: Inst) u32;
      externfnanalyze_alloc(inst: Inst) u32;
      externfnanalyze_alloc_mut(inst: Inst) u32;
      externfnanalyze_alloc_inferred(inst: Inst) u32;
      externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
      externfnanalyze_anyframe_type(inst: Inst) u32;
      externfnanalyze_array_cat(inst: Inst) u32;
      externfnanalyze_array_mul(inst: Inst) u32;

      Snippet of assembly:

      .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

      Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

      EDIT: New Discovery

      It does not get much simpler than this

      Wrong!

      After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

      constInst=externstruct {
      tag: Tag,
      constTag=externenum {
      end,
      add,
      addwrap,
      alloc,
      alloc_mut,
      alloc_inferred,
      alloc_inferred_mut,
      anyframe_type,
      array_cat,
      array_mul,
      };
      };
      exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
      vari: usize=0;
      while (true) {
      constinst=inst_list[i];
      switch (inst.tag) {
      .end=>return,
      .add=> {
      map[i] =analyze_add(inst);
      i+=1;
      continue;
      },
      .addwrap=> {
      map[i] =analyze_addwrap(inst);
      i+=1;
      continue;
      },
      .alloc=> {
      map[i] =analyze_alloc(inst);
      i+=1;
      continue;
      },
      .alloc_mut=> {
      map[i] =analyze_alloc_mut(inst);
      i+=1;
      continue;
      },
      .alloc_inferred=> {
      map[i] =analyze_alloc_inferred(inst);
      i+=1;
      continue;
      },
      .alloc_inferred_mut=> {
      map[i] =analyze_alloc_inferred_mut(inst);
      i+=1;
      continue;
      },
      .anyframe_type=> {
      map[i] =analyze_anyframe_type(inst);
      i+=1;
      continue;
      },
      .array_cat=> {
      map[i] =analyze_array_cat(inst);
      i+=1;
      continue;
      },
      .array_mul=> {
      map[i] =analyze_array_mul(inst);
      i+=1;
      continue;
      },
      }
      break;
      }
      }
      externfnanalyze_add(inst: Inst) u32;
      externfnanalyze_addwrap(inst: Inst) u32;
      externfnanalyze_alloc(inst: Inst) u32;
      externfnanalyze_alloc_mut(inst: Inst) u32;
      externfnanalyze_alloc_inferred(inst: Inst) u32;
      externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
      externfnanalyze_anyframe_type(inst: Inst) u32;
      externfnanalyze_array_cat(inst: Inst) u32;
      externfnanalyze_array_mul(inst: Inst) u32;

      Two example prongs from the machine code:

      .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

      It's perfect! This is exactly what we wanted.

      This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

      Real Actual Use Case

      Here's one in the self-hosted compiler:

      switch (old_inst.tag) {

      This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

      This pattern also exists in:

      • The tokenizer
      • The parser
      • astgen
      • sema (this is the linked one above)
      • codegen
      • translate-c
      • zig fmt

      (pretty much in every stage of the pipeline)

      Other Possible Solution: Tail Calls

      Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

      This is reasonable in the sense that it is doable right now; however there are some problems:

      • As far as I understand, tail calls don't work on some architectures.
        • (what are these? does anybody know?)
      • I'm also concerned about trying to debug when doing dispatch with tail calls.
      • It forces you to organize your logic into functions. That's another jump that
        maybe you did not want in your hot path.

      Proposal

      I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

      constInst=externstruct {
      tag: Tag,
      constTag=externenum {
      end,
      add,
      addwrap,
      alloc,
      alloc_mut,
      alloc_inferred,
      alloc_inferred_mut,
      anyframe_type,
      array_cat,
      array_mul,
      };
      };
      exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
      vari: usize=0;
      sw: switch (inst_list[i].tag) {
      .end=>return,
      .add=> {
      map[i] =analyze_add(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .addwrap=> {
      map[i] =analyze_addwrap(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .alloc=> {
      map[i] =analyze_alloc(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .alloc_mut=> {
      map[i] =analyze_alloc_mut(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .alloc_inferred=> {
      map[i] =analyze_alloc_inferred(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .alloc_inferred_mut=> {
      map[i] =analyze_alloc_inferred_mut(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .anyframe_type=> {
      map[i] =analyze_anyframe_type(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .array_cat=> {
      map[i] =analyze_array_cat(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      .array_mul=> {
      map[i] =analyze_array_mul(inst_list[i]);
      i+=1;
      continue :swinst_list[i].tag;
      },
      }
      }
      externfnanalyze_add(inst: Inst) u32;
      externfnanalyze_addwrap(inst: Inst) u32;
      externfnanalyze_alloc(inst: Inst) u32;
      externfnanalyze_alloc_mut(inst: Inst) u32;
      externfnanalyze_alloc_inferred(inst: Inst) u32;
      externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
      externfnanalyze_anyframe_type(inst: Inst) u32;
      externfnanalyze_array_cat(inst: Inst) u32;
      externfnanalyze_array_mul(inst: Inst) u32;

      The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

      • labeled continue with an operand on a loop would be a compile error
      • labeled break with a switch would be OK.

      How to Lower this to LLVM

      Note: I wrote this section before the EDIT: New Discovery section.

      One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

      SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
      storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
      i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
      ]
      SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
      storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
      i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
      ]

      The machine code for the prongs looks like this:

      <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

      Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

      .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

      The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

      Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

      How to Lower this in Self-Hosted Backends

      We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

      OK But Is The Perf Actually Good?

      I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

        Type

        No type

        Projects

        No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
        Skip to content

        introduce labeled continue syntax inside a switch expression #8220

        Description

        @andrewrk

        Background

        The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

        However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

        Problem Statement

        For example (godbolt link):

        constInst=externstruct {
        tag: Tag,
        constTag=externenum {
        add,
        addwrap,
        alloc,
        alloc_mut,
        alloc_inferred,
        alloc_inferred_mut,
        anyframe_type,
        array_cat,
        array_mul,
        array_type,
        array_type_sentinel,
        indexable_ptr_len,
        };
        };
        exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
        constinst_list=inst_list_ptr[0..inst_list_len];
        for (inst_list) |inst, i| {
        map[i] =switch (inst.tag) {
        .add=>analyze_add(inst),
        .addwrap=>analyze_addwrap(inst),
        .alloc=>analyze_alloc(inst),
        .alloc_mut=>analyze_alloc_mut(inst),
        .alloc_inferred=>analyze_alloc_inferred(inst),
        .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
        .anyframe_type=>analyze_anyframe_type(inst),
        .array_cat=>analyze_array_cat(inst),
        .array_mul=>analyze_array_mul(inst),
        .array_type=>analyze_array_type(inst),
        .array_type_sentinel=>analyze_array_type_sentinel(inst),
        .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
        };
        }
        }
        externfnanalyze_add(inst: Inst) u32;
        externfnanalyze_addwrap(inst: Inst) u32;
        externfnanalyze_alloc(inst: Inst) u32;
        externfnanalyze_alloc_mut(inst: Inst) u32;
        externfnanalyze_alloc_inferred(inst: Inst) u32;
        externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
        externfnanalyze_anyframe_type(inst: Inst) u32;
        externfnanalyze_array_cat(inst: Inst) u32;
        externfnanalyze_array_mul(inst: Inst) u32;
        externfnanalyze_array_type(inst: Inst) u32;
        externfnanalyze_array_type_sentinel(inst: Inst) u32;
        externfnanalyze_indexable_ptr_len(inst: Inst) u32;

        In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

        .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

        The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

        Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

        This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

        They explain it in a nice, intuitive way here:

        Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

        So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

        In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

        Research Dump

        Can LLVM Do the Optimization?

        In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

        constInst=externstruct {
        tag: Tag,
        constTag=externenum {
        add,
        addwrap,
        alloc,
        alloc_mut,
        alloc_inferred,
        alloc_inferred_mut,
        anyframe_type,
        array_cat,
        array_mul,
        };
        };
        exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
        constinst_list=inst_list_ptr[0..inst_list_len];
        vari: usize=0;
        while (true) {
        constinst=inst_list[i];
        switch (inst.tag) {
        .add=> {
        map[i] =analyze_add(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .addwrap=> {
        map[i] =analyze_addwrap(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .alloc=> {
        map[i] =analyze_alloc(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .alloc_mut=> {
        map[i] =analyze_alloc_mut(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .alloc_inferred=> {
        map[i] =analyze_alloc_inferred(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .alloc_inferred_mut=> {
        map[i] =analyze_alloc_inferred_mut(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .anyframe_type=> {
        map[i] =analyze_anyframe_type(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .array_cat=> {
        map[i] =analyze_array_cat(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        .array_mul=> {
        map[i] =analyze_array_mul(inst);
        i+=1;
        if (i<inst_list_len) continue;
        },
        }
        break;
        }
        }
        externfnanalyze_add(inst: Inst) u32;
        externfnanalyze_addwrap(inst: Inst) u32;
        externfnanalyze_alloc(inst: Inst) u32;
        externfnanalyze_alloc_mut(inst: Inst) u32;
        externfnanalyze_alloc_inferred(inst: Inst) u32;
        externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
        externfnanalyze_anyframe_type(inst: Inst) u32;
        externfnanalyze_array_cat(inst: Inst) u32;
        externfnanalyze_array_mul(inst: Inst) u32;

        Snippet of assembly:

        .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

        Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

        EDIT: New Discovery

        It does not get much simpler than this

        Wrong!

        After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

        constInst=externstruct {
        tag: Tag,
        constTag=externenum {
        end,
        add,
        addwrap,
        alloc,
        alloc_mut,
        alloc_inferred,
        alloc_inferred_mut,
        anyframe_type,
        array_cat,
        array_mul,
        };
        };
        exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
        vari: usize=0;
        while (true) {
        constinst=inst_list[i];
        switch (inst.tag) {
        .end=>return,
        .add=> {
        map[i] =analyze_add(inst);
        i+=1;
        continue;
        },
        .addwrap=> {
        map[i] =analyze_addwrap(inst);
        i+=1;
        continue;
        },
        .alloc=> {
        map[i] =analyze_alloc(inst);
        i+=1;
        continue;
        },
        .alloc_mut=> {
        map[i] =analyze_alloc_mut(inst);
        i+=1;
        continue;
        },
        .alloc_inferred=> {
        map[i] =analyze_alloc_inferred(inst);
        i+=1;
        continue;
        },
        .alloc_inferred_mut=> {
        map[i] =analyze_alloc_inferred_mut(inst);
        i+=1;
        continue;
        },
        .anyframe_type=> {
        map[i] =analyze_anyframe_type(inst);
        i+=1;
        continue;
        },
        .array_cat=> {
        map[i] =analyze_array_cat(inst);
        i+=1;
        continue;
        },
        .array_mul=> {
        map[i] =analyze_array_mul(inst);
        i+=1;
        continue;
        },
        }
        break;
        }
        }
        externfnanalyze_add(inst: Inst) u32;
        externfnanalyze_addwrap(inst: Inst) u32;
        externfnanalyze_alloc(inst: Inst) u32;
        externfnanalyze_alloc_mut(inst: Inst) u32;
        externfnanalyze_alloc_inferred(inst: Inst) u32;
        externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
        externfnanalyze_anyframe_type(inst: Inst) u32;
        externfnanalyze_array_cat(inst: Inst) u32;
        externfnanalyze_array_mul(inst: Inst) u32;

        Two example prongs from the machine code:

        .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

        It's perfect! This is exactly what we wanted.

        This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

        Real Actual Use Case

        Here's one in the self-hosted compiler:

        switch (old_inst.tag) {

        This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

        This pattern also exists in:

        • The tokenizer
        • The parser
        • astgen
        • sema (this is the linked one above)
        • codegen
        • translate-c
        • zig fmt

        (pretty much in every stage of the pipeline)

        Other Possible Solution: Tail Calls

        Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

        This is reasonable in the sense that it is doable right now; however there are some problems:

        • As far as I understand, tail calls don't work on some architectures.
          • (what are these? does anybody know?)
        • I'm also concerned about trying to debug when doing dispatch with tail calls.
        • It forces you to organize your logic into functions. That's another jump that
          maybe you did not want in your hot path.

        Proposal

        I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

        constInst=externstruct {
        tag: Tag,
        constTag=externenum {
        end,
        add,
        addwrap,
        alloc,
        alloc_mut,
        alloc_inferred,
        alloc_inferred_mut,
        anyframe_type,
        array_cat,
        array_mul,
        };
        };
        exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
        vari: usize=0;
        sw: switch (inst_list[i].tag) {
        .end=>return,
        .add=> {
        map[i] =analyze_add(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .addwrap=> {
        map[i] =analyze_addwrap(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .alloc=> {
        map[i] =analyze_alloc(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .alloc_mut=> {
        map[i] =analyze_alloc_mut(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .alloc_inferred=> {
        map[i] =analyze_alloc_inferred(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .alloc_inferred_mut=> {
        map[i] =analyze_alloc_inferred_mut(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .anyframe_type=> {
        map[i] =analyze_anyframe_type(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .array_cat=> {
        map[i] =analyze_array_cat(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        .array_mul=> {
        map[i] =analyze_array_mul(inst_list[i]);
        i+=1;
        continue :swinst_list[i].tag;
        },
        }
        }
        externfnanalyze_add(inst: Inst) u32;
        externfnanalyze_addwrap(inst: Inst) u32;
        externfnanalyze_alloc(inst: Inst) u32;
        externfnanalyze_alloc_mut(inst: Inst) u32;
        externfnanalyze_alloc_inferred(inst: Inst) u32;
        externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
        externfnanalyze_anyframe_type(inst: Inst) u32;
        externfnanalyze_array_cat(inst: Inst) u32;
        externfnanalyze_array_mul(inst: Inst) u32;

        The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

        • labeled continue with an operand on a loop would be a compile error
        • labeled break with a switch would be OK.

        How to Lower this to LLVM

        Note: I wrote this section before the EDIT: New Discovery section.

        One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

        SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
        storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
        i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
        ]
        SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
        storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
        i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
        ]

        The machine code for the prongs looks like this:

        <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

        Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

        .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

        The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

        Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

        How to Lower this in Self-Hosted Backends

        We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

        OK But Is The Perf Actually Good?

        I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

        Metadata

        Metadata

        Assignees

        No one assigned

          Labels

          acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

          Type

          No type

          Projects

          No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
          Skip to content

          introduce labeled continue syntax inside a switch expression #8220

          Description

          @andrewrk

          Background

          The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

          However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

          Problem Statement

          For example (godbolt link):

          constInst=externstruct {
          tag: Tag,
          constTag=externenum {
          add,
          addwrap,
          alloc,
          alloc_mut,
          alloc_inferred,
          alloc_inferred_mut,
          anyframe_type,
          array_cat,
          array_mul,
          array_type,
          array_type_sentinel,
          indexable_ptr_len,
          };
          };
          exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
          constinst_list=inst_list_ptr[0..inst_list_len];
          for (inst_list) |inst, i| {
          map[i] =switch (inst.tag) {
          .add=>analyze_add(inst),
          .addwrap=>analyze_addwrap(inst),
          .alloc=>analyze_alloc(inst),
          .alloc_mut=>analyze_alloc_mut(inst),
          .alloc_inferred=>analyze_alloc_inferred(inst),
          .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
          .anyframe_type=>analyze_anyframe_type(inst),
          .array_cat=>analyze_array_cat(inst),
          .array_mul=>analyze_array_mul(inst),
          .array_type=>analyze_array_type(inst),
          .array_type_sentinel=>analyze_array_type_sentinel(inst),
          .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
          };
          }
          }
          externfnanalyze_add(inst: Inst) u32;
          externfnanalyze_addwrap(inst: Inst) u32;
          externfnanalyze_alloc(inst: Inst) u32;
          externfnanalyze_alloc_mut(inst: Inst) u32;
          externfnanalyze_alloc_inferred(inst: Inst) u32;
          externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
          externfnanalyze_anyframe_type(inst: Inst) u32;
          externfnanalyze_array_cat(inst: Inst) u32;
          externfnanalyze_array_mul(inst: Inst) u32;
          externfnanalyze_array_type(inst: Inst) u32;
          externfnanalyze_array_type_sentinel(inst: Inst) u32;
          externfnanalyze_indexable_ptr_len(inst: Inst) u32;

          In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

          .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

          The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

          Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

          This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

          They explain it in a nice, intuitive way here:

          Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

          So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

          In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

          Research Dump

          Can LLVM Do the Optimization?

          In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

          constInst=externstruct {
          tag: Tag,
          constTag=externenum {
          add,
          addwrap,
          alloc,
          alloc_mut,
          alloc_inferred,
          alloc_inferred_mut,
          anyframe_type,
          array_cat,
          array_mul,
          };
          };
          exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
          constinst_list=inst_list_ptr[0..inst_list_len];
          vari: usize=0;
          while (true) {
          constinst=inst_list[i];
          switch (inst.tag) {
          .add=> {
          map[i] =analyze_add(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .addwrap=> {
          map[i] =analyze_addwrap(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .alloc=> {
          map[i] =analyze_alloc(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .alloc_mut=> {
          map[i] =analyze_alloc_mut(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .alloc_inferred=> {
          map[i] =analyze_alloc_inferred(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .alloc_inferred_mut=> {
          map[i] =analyze_alloc_inferred_mut(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .anyframe_type=> {
          map[i] =analyze_anyframe_type(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .array_cat=> {
          map[i] =analyze_array_cat(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          .array_mul=> {
          map[i] =analyze_array_mul(inst);
          i+=1;
          if (i<inst_list_len) continue;
          },
          }
          break;
          }
          }
          externfnanalyze_add(inst: Inst) u32;
          externfnanalyze_addwrap(inst: Inst) u32;
          externfnanalyze_alloc(inst: Inst) u32;
          externfnanalyze_alloc_mut(inst: Inst) u32;
          externfnanalyze_alloc_inferred(inst: Inst) u32;
          externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
          externfnanalyze_anyframe_type(inst: Inst) u32;
          externfnanalyze_array_cat(inst: Inst) u32;
          externfnanalyze_array_mul(inst: Inst) u32;

          Snippet of assembly:

          .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

          Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

          EDIT: New Discovery

          It does not get much simpler than this

          Wrong!

          After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

          constInst=externstruct {
          tag: Tag,
          constTag=externenum {
          end,
          add,
          addwrap,
          alloc,
          alloc_mut,
          alloc_inferred,
          alloc_inferred_mut,
          anyframe_type,
          array_cat,
          array_mul,
          };
          };
          exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
          vari: usize=0;
          while (true) {
          constinst=inst_list[i];
          switch (inst.tag) {
          .end=>return,
          .add=> {
          map[i] =analyze_add(inst);
          i+=1;
          continue;
          },
          .addwrap=> {
          map[i] =analyze_addwrap(inst);
          i+=1;
          continue;
          },
          .alloc=> {
          map[i] =analyze_alloc(inst);
          i+=1;
          continue;
          },
          .alloc_mut=> {
          map[i] =analyze_alloc_mut(inst);
          i+=1;
          continue;
          },
          .alloc_inferred=> {
          map[i] =analyze_alloc_inferred(inst);
          i+=1;
          continue;
          },
          .alloc_inferred_mut=> {
          map[i] =analyze_alloc_inferred_mut(inst);
          i+=1;
          continue;
          },
          .anyframe_type=> {
          map[i] =analyze_anyframe_type(inst);
          i+=1;
          continue;
          },
          .array_cat=> {
          map[i] =analyze_array_cat(inst);
          i+=1;
          continue;
          },
          .array_mul=> {
          map[i] =analyze_array_mul(inst);
          i+=1;
          continue;
          },
          }
          break;
          }
          }
          externfnanalyze_add(inst: Inst) u32;
          externfnanalyze_addwrap(inst: Inst) u32;
          externfnanalyze_alloc(inst: Inst) u32;
          externfnanalyze_alloc_mut(inst: Inst) u32;
          externfnanalyze_alloc_inferred(inst: Inst) u32;
          externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
          externfnanalyze_anyframe_type(inst: Inst) u32;
          externfnanalyze_array_cat(inst: Inst) u32;
          externfnanalyze_array_mul(inst: Inst) u32;

          Two example prongs from the machine code:

          .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

          It's perfect! This is exactly what we wanted.

          This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

          Real Actual Use Case

          Here's one in the self-hosted compiler:

          switch (old_inst.tag) {

          This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

          This pattern also exists in:

          • The tokenizer
          • The parser
          • astgen
          • sema (this is the linked one above)
          • codegen
          • translate-c
          • zig fmt

          (pretty much in every stage of the pipeline)

          Other Possible Solution: Tail Calls

          Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

          This is reasonable in the sense that it is doable right now; however there are some problems:

          • As far as I understand, tail calls don't work on some architectures.
            • (what are these? does anybody know?)
          • I'm also concerned about trying to debug when doing dispatch with tail calls.
          • It forces you to organize your logic into functions. That's another jump that
            maybe you did not want in your hot path.

          Proposal

          I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

          constInst=externstruct {
          tag: Tag,
          constTag=externenum {
          end,
          add,
          addwrap,
          alloc,
          alloc_mut,
          alloc_inferred,
          alloc_inferred_mut,
          anyframe_type,
          array_cat,
          array_mul,
          };
          };
          exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
          vari: usize=0;
          sw: switch (inst_list[i].tag) {
          .end=>return,
          .add=> {
          map[i] =analyze_add(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .addwrap=> {
          map[i] =analyze_addwrap(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .alloc=> {
          map[i] =analyze_alloc(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .alloc_mut=> {
          map[i] =analyze_alloc_mut(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .alloc_inferred=> {
          map[i] =analyze_alloc_inferred(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .alloc_inferred_mut=> {
          map[i] =analyze_alloc_inferred_mut(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .anyframe_type=> {
          map[i] =analyze_anyframe_type(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .array_cat=> {
          map[i] =analyze_array_cat(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          .array_mul=> {
          map[i] =analyze_array_mul(inst_list[i]);
          i+=1;
          continue :swinst_list[i].tag;
          },
          }
          }
          externfnanalyze_add(inst: Inst) u32;
          externfnanalyze_addwrap(inst: Inst) u32;
          externfnanalyze_alloc(inst: Inst) u32;
          externfnanalyze_alloc_mut(inst: Inst) u32;
          externfnanalyze_alloc_inferred(inst: Inst) u32;
          externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
          externfnanalyze_anyframe_type(inst: Inst) u32;
          externfnanalyze_array_cat(inst: Inst) u32;
          externfnanalyze_array_mul(inst: Inst) u32;

          The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

          • labeled continue with an operand on a loop would be a compile error
          • labeled break with a switch would be OK.

          How to Lower this to LLVM

          Note: I wrote this section before the EDIT: New Discovery section.

          One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

          SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
          storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
          i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
          ]
          SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
          storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
          i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
          ]

          The machine code for the prongs looks like this:

          <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

          Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

          .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

          The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

          Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

          How to Lower this in Self-Hosted Backends

          We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

          OK But Is The Perf Actually Good?

          I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

            Type

            No type

            Projects

            No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
            Skip to content

            introduce labeled continue syntax inside a switch expression #8220

            Description

            @andrewrk

            Background

            The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

            However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

            Problem Statement

            For example (godbolt link):

            constInst=externstruct {
            tag: Tag,
            constTag=externenum {
            add,
            addwrap,
            alloc,
            alloc_mut,
            alloc_inferred,
            alloc_inferred_mut,
            anyframe_type,
            array_cat,
            array_mul,
            array_type,
            array_type_sentinel,
            indexable_ptr_len,
            };
            };
            exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
            constinst_list=inst_list_ptr[0..inst_list_len];
            for (inst_list) |inst, i| {
            map[i] =switch (inst.tag) {
            .add=>analyze_add(inst),
            .addwrap=>analyze_addwrap(inst),
            .alloc=>analyze_alloc(inst),
            .alloc_mut=>analyze_alloc_mut(inst),
            .alloc_inferred=>analyze_alloc_inferred(inst),
            .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
            .anyframe_type=>analyze_anyframe_type(inst),
            .array_cat=>analyze_array_cat(inst),
            .array_mul=>analyze_array_mul(inst),
            .array_type=>analyze_array_type(inst),
            .array_type_sentinel=>analyze_array_type_sentinel(inst),
            .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
            };
            }
            }
            externfnanalyze_add(inst: Inst) u32;
            externfnanalyze_addwrap(inst: Inst) u32;
            externfnanalyze_alloc(inst: Inst) u32;
            externfnanalyze_alloc_mut(inst: Inst) u32;
            externfnanalyze_alloc_inferred(inst: Inst) u32;
            externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
            externfnanalyze_anyframe_type(inst: Inst) u32;
            externfnanalyze_array_cat(inst: Inst) u32;
            externfnanalyze_array_mul(inst: Inst) u32;
            externfnanalyze_array_type(inst: Inst) u32;
            externfnanalyze_array_type_sentinel(inst: Inst) u32;
            externfnanalyze_indexable_ptr_len(inst: Inst) u32;

            In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

            .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

            The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

            Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

            This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

            They explain it in a nice, intuitive way here:

            Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

            So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

            In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

            Research Dump

            Can LLVM Do the Optimization?

            In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

            constInst=externstruct {
            tag: Tag,
            constTag=externenum {
            add,
            addwrap,
            alloc,
            alloc_mut,
            alloc_inferred,
            alloc_inferred_mut,
            anyframe_type,
            array_cat,
            array_mul,
            };
            };
            exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
            constinst_list=inst_list_ptr[0..inst_list_len];
            vari: usize=0;
            while (true) {
            constinst=inst_list[i];
            switch (inst.tag) {
            .add=> {
            map[i] =analyze_add(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .addwrap=> {
            map[i] =analyze_addwrap(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .alloc=> {
            map[i] =analyze_alloc(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .alloc_mut=> {
            map[i] =analyze_alloc_mut(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .alloc_inferred=> {
            map[i] =analyze_alloc_inferred(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .alloc_inferred_mut=> {
            map[i] =analyze_alloc_inferred_mut(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .anyframe_type=> {
            map[i] =analyze_anyframe_type(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .array_cat=> {
            map[i] =analyze_array_cat(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            .array_mul=> {
            map[i] =analyze_array_mul(inst);
            i+=1;
            if (i<inst_list_len) continue;
            },
            }
            break;
            }
            }
            externfnanalyze_add(inst: Inst) u32;
            externfnanalyze_addwrap(inst: Inst) u32;
            externfnanalyze_alloc(inst: Inst) u32;
            externfnanalyze_alloc_mut(inst: Inst) u32;
            externfnanalyze_alloc_inferred(inst: Inst) u32;
            externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
            externfnanalyze_anyframe_type(inst: Inst) u32;
            externfnanalyze_array_cat(inst: Inst) u32;
            externfnanalyze_array_mul(inst: Inst) u32;

            Snippet of assembly:

            .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

            Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

            EDIT: New Discovery

            It does not get much simpler than this

            Wrong!

            After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

            constInst=externstruct {
            tag: Tag,
            constTag=externenum {
            end,
            add,
            addwrap,
            alloc,
            alloc_mut,
            alloc_inferred,
            alloc_inferred_mut,
            anyframe_type,
            array_cat,
            array_mul,
            };
            };
            exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
            vari: usize=0;
            while (true) {
            constinst=inst_list[i];
            switch (inst.tag) {
            .end=>return,
            .add=> {
            map[i] =analyze_add(inst);
            i+=1;
            continue;
            },
            .addwrap=> {
            map[i] =analyze_addwrap(inst);
            i+=1;
            continue;
            },
            .alloc=> {
            map[i] =analyze_alloc(inst);
            i+=1;
            continue;
            },
            .alloc_mut=> {
            map[i] =analyze_alloc_mut(inst);
            i+=1;
            continue;
            },
            .alloc_inferred=> {
            map[i] =analyze_alloc_inferred(inst);
            i+=1;
            continue;
            },
            .alloc_inferred_mut=> {
            map[i] =analyze_alloc_inferred_mut(inst);
            i+=1;
            continue;
            },
            .anyframe_type=> {
            map[i] =analyze_anyframe_type(inst);
            i+=1;
            continue;
            },
            .array_cat=> {
            map[i] =analyze_array_cat(inst);
            i+=1;
            continue;
            },
            .array_mul=> {
            map[i] =analyze_array_mul(inst);
            i+=1;
            continue;
            },
            }
            break;
            }
            }
            externfnanalyze_add(inst: Inst) u32;
            externfnanalyze_addwrap(inst: Inst) u32;
            externfnanalyze_alloc(inst: Inst) u32;
            externfnanalyze_alloc_mut(inst: Inst) u32;
            externfnanalyze_alloc_inferred(inst: Inst) u32;
            externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
            externfnanalyze_anyframe_type(inst: Inst) u32;
            externfnanalyze_array_cat(inst: Inst) u32;
            externfnanalyze_array_mul(inst: Inst) u32;

            Two example prongs from the machine code:

            .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

            It's perfect! This is exactly what we wanted.

            This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

            Real Actual Use Case

            Here's one in the self-hosted compiler:

            switch (old_inst.tag) {

            This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

            This pattern also exists in:

            • The tokenizer
            • The parser
            • astgen
            • sema (this is the linked one above)
            • codegen
            • translate-c
            • zig fmt

            (pretty much in every stage of the pipeline)

            Other Possible Solution: Tail Calls

            Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

            This is reasonable in the sense that it is doable right now; however there are some problems:

            • As far as I understand, tail calls don't work on some architectures.
              • (what are these? does anybody know?)
            • I'm also concerned about trying to debug when doing dispatch with tail calls.
            • It forces you to organize your logic into functions. That's another jump that
              maybe you did not want in your hot path.

            Proposal

            I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

            constInst=externstruct {
            tag: Tag,
            constTag=externenum {
            end,
            add,
            addwrap,
            alloc,
            alloc_mut,
            alloc_inferred,
            alloc_inferred_mut,
            anyframe_type,
            array_cat,
            array_mul,
            };
            };
            exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
            vari: usize=0;
            sw: switch (inst_list[i].tag) {
            .end=>return,
            .add=> {
            map[i] =analyze_add(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .addwrap=> {
            map[i] =analyze_addwrap(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .alloc=> {
            map[i] =analyze_alloc(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .alloc_mut=> {
            map[i] =analyze_alloc_mut(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .alloc_inferred=> {
            map[i] =analyze_alloc_inferred(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .alloc_inferred_mut=> {
            map[i] =analyze_alloc_inferred_mut(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .anyframe_type=> {
            map[i] =analyze_anyframe_type(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .array_cat=> {
            map[i] =analyze_array_cat(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            .array_mul=> {
            map[i] =analyze_array_mul(inst_list[i]);
            i+=1;
            continue :swinst_list[i].tag;
            },
            }
            }
            externfnanalyze_add(inst: Inst) u32;
            externfnanalyze_addwrap(inst: Inst) u32;
            externfnanalyze_alloc(inst: Inst) u32;
            externfnanalyze_alloc_mut(inst: Inst) u32;
            externfnanalyze_alloc_inferred(inst: Inst) u32;
            externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
            externfnanalyze_anyframe_type(inst: Inst) u32;
            externfnanalyze_array_cat(inst: Inst) u32;
            externfnanalyze_array_mul(inst: Inst) u32;

            The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

            • labeled continue with an operand on a loop would be a compile error
            • labeled break with a switch would be OK.

            How to Lower this to LLVM

            Note: I wrote this section before the EDIT: New Discovery section.

            One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

            SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
            storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
            i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
            ]
            SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
            storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
            i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
            ]

            The machine code for the prongs looks like this:

            <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

            Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

            .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

            The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

            Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

            How to Lower this in Self-Hosted Backends

            We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

            OK But Is The Perf Actually Good?

            I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

            Metadata

            Metadata

            Assignees

            No one assigned

              Labels

              acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

              Type

              No type

              Projects

              No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); introduce labeled continue syntax inside a switch expression · Issue #8220 · ziglang/zig · GitHub
              Skip to content

              introduce labeled continue syntax inside a switch expression #8220

              Description

              @andrewrk

              Background

              The goto keyword was removed in #630. This remains the right call because all the control flow in Zig can be expressed in a better way: continue to goto backwards, and break to goto forwards.

              However, in C, there is another concept, called "computed goto". This is described in #5950 and briefly discussed in #2162. This concept is not currently possible in Zig. It is possible to model the desired semantics with existing control flow features quite simply, but it is not possible to obtain the desired machine code, even in optimized builds.

              Problem Statement

              For example (godbolt link):

              constInst=externstruct {
              tag: Tag,
              constTag=externenum {
              add,
              addwrap,
              alloc,
              alloc_mut,
              alloc_inferred,
              alloc_inferred_mut,
              anyframe_type,
              array_cat,
              array_mul,
              array_type,
              array_type_sentinel,
              indexable_ptr_len,
              };
              };
              exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
              constinst_list=inst_list_ptr[0..inst_list_len];
              for (inst_list) |inst, i| {
              map[i] =switch (inst.tag) {
              .add=>analyze_add(inst),
              .addwrap=>analyze_addwrap(inst),
              .alloc=>analyze_alloc(inst),
              .alloc_mut=>analyze_alloc_mut(inst),
              .alloc_inferred=>analyze_alloc_inferred(inst),
              .alloc_inferred_mut=>analyze_alloc_inferred_mut(inst),
              .anyframe_type=>analyze_anyframe_type(inst),
              .array_cat=>analyze_array_cat(inst),
              .array_mul=>analyze_array_mul(inst),
              .array_type=>analyze_array_type(inst),
              .array_type_sentinel=>analyze_array_type_sentinel(inst),
              .indexable_ptr_len=>analyze_indexable_ptr_len(inst),
              };
              }
              }
              externfnanalyze_add(inst: Inst) u32;
              externfnanalyze_addwrap(inst: Inst) u32;
              externfnanalyze_alloc(inst: Inst) u32;
              externfnanalyze_alloc_mut(inst: Inst) u32;
              externfnanalyze_alloc_inferred(inst: Inst) u32;
              externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
              externfnanalyze_anyframe_type(inst: Inst) u32;
              externfnanalyze_array_cat(inst: Inst) u32;
              externfnanalyze_array_mul(inst: Inst) u32;
              externfnanalyze_array_type(inst: Inst) u32;
              externfnanalyze_array_type_sentinel(inst: Inst) u32;
              externfnanalyze_indexable_ptr_len(inst: Inst) u32;

              In the generated machine code, each prong ends up jumping back to the loop condition, before getting re-dispatched to the next prong:

              .LBB0_3:xoredi,edicall analyze_addjmp .LBB0_15.LBB0_4:movedi,1call analyze_addwrapjmp .LBB0_15

              The reason this machine code is not what we desire is described in this paper in the section "Direct Threading" and "The Context Problem":

              Mispredicted branches pose a serious challenge to modern processors because they threaten to starve the processor of instructions. The problem is that before the destination of the branch is known the execution of the pipeline may run dry. To perform at full speed, modern CPUs need to keep their pipelines full by correctly predicting branch targets.

              This problem is even worse for direct call threading and switch dispatch. For these techniques there is only one dispatch branch and so all dispatches share the same BTB entry. Direct call threading will mispredict all dispatches except when the same virtual instruction body is dispatched multiple times consecutively.

              They explain it in a nice, intuitive way here:

              Another perspective is that the destination of the indirect dispatch branch is unpredictable because its destination is not correlated with the hardware pc. Instead, its destination is correlated to the vPC. We refer to this lack of correlation between the hardware pc and vPC as the context problem. We choose the term context following its use in context sensitive inlining [#!Grove_Chambers_2002!#] because in both cases the context of shared code (in their case methods, in our case virtual instruction bodies) is important to consider.

              So the problem statement here is that we want to be able to write zig code that outputs machine code that matches this Direct Threading pattern. In one sense, it is an optimization problem, since we can model the same semantics with other language constructs and other machine code. But in another sense, it is more fundamental than an optimization problem, because Zig is a language that wants to generate optimal machine code, meaning it is possible to write Zig code that generates machine code equivalent or better to what you could write by hand.

              In short summary, we want to be able to express zig code where each switch prong jumps directly to the next prong, instead of all switch prongs sharing the same indirect jump, in order to benefit the branch predictor.

              Research Dump

              Can LLVM Do the Optimization?

              In this example (godbolt link), I changed the loop to while(true) and manually inlined the continue expression into each switch prong, with a continue. It does not get much simpler than this; we are practically begging LLVM to do the optimization.

              constInst=externstruct {
              tag: Tag,
              constTag=externenum {
              add,
              addwrap,
              alloc,
              alloc_mut,
              alloc_inferred,
              alloc_inferred_mut,
              anyframe_type,
              array_cat,
              array_mul,
              };
              };
              exportfnentry(inst_list_ptr: [*]constInst, inst_list_len: usize, map: [*]u32) void {
              constinst_list=inst_list_ptr[0..inst_list_len];
              vari: usize=0;
              while (true) {
              constinst=inst_list[i];
              switch (inst.tag) {
              .add=> {
              map[i] =analyze_add(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .addwrap=> {
              map[i] =analyze_addwrap(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .alloc=> {
              map[i] =analyze_alloc(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .alloc_mut=> {
              map[i] =analyze_alloc_mut(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .alloc_inferred=> {
              map[i] =analyze_alloc_inferred(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .alloc_inferred_mut=> {
              map[i] =analyze_alloc_inferred_mut(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .anyframe_type=> {
              map[i] =analyze_anyframe_type(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .array_cat=> {
              map[i] =analyze_array_cat(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              .array_mul=> {
              map[i] =analyze_array_mul(inst);
              i+=1;
              if (i<inst_list_len) continue;
              },
              }
              break;
              }
              }
              externfnanalyze_add(inst: Inst) u32;
              externfnanalyze_addwrap(inst: Inst) u32;
              externfnanalyze_alloc(inst: Inst) u32;
              externfnanalyze_alloc_mut(inst: Inst) u32;
              externfnanalyze_alloc_inferred(inst: Inst) u32;
              externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
              externfnanalyze_anyframe_type(inst: Inst) u32;
              externfnanalyze_array_cat(inst: Inst) u32;
              externfnanalyze_array_mul(inst: Inst) u32;

              Snippet of assembly:

              .LBB0_3:mov dword ptr [r14+4*rbx],eaxincrbxcmprbx,r15jae .LBB0_4.LBB0_1:moveax, dword ptr [r12+4*rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_2:xoredi,edicall analyze_addjmp .LBB0_3.LBB0_6:movedi,2call analyze_allocjmp .LBB0_3.LBB0_5:movedi,1call analyze_addwrapjmp .LBB0_3.LBB0_7:movedi,3call analyze_alloc_mutjmp .LBB0_3

              Here, LLVM actually figured out the continue expression was duplicated N times, and un-inlined it, putting the code back how it was! So crafty.

              EDIT: New Discovery

              It does not get much simpler than this

              Wrong!

              After typing up this whole proposal, I realized that I did not try that optimization with using an "end" tag in the above code. Here is the case, modified (godbolt link):

              constInst=externstruct {
              tag: Tag,
              constTag=externenum {
              end,
              add,
              addwrap,
              alloc,
              alloc_mut,
              alloc_inferred,
              alloc_inferred_mut,
              anyframe_type,
              array_cat,
              array_mul,
              };
              };
              exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
              vari: usize=0;
              while (true) {
              constinst=inst_list[i];
              switch (inst.tag) {
              .end=>return,
              .add=> {
              map[i] =analyze_add(inst);
              i+=1;
              continue;
              },
              .addwrap=> {
              map[i] =analyze_addwrap(inst);
              i+=1;
              continue;
              },
              .alloc=> {
              map[i] =analyze_alloc(inst);
              i+=1;
              continue;
              },
              .alloc_mut=> {
              map[i] =analyze_alloc_mut(inst);
              i+=1;
              continue;
              },
              .alloc_inferred=> {
              map[i] =analyze_alloc_inferred(inst);
              i+=1;
              continue;
              },
              .alloc_inferred_mut=> {
              map[i] =analyze_alloc_inferred_mut(inst);
              i+=1;
              continue;
              },
              .anyframe_type=> {
              map[i] =analyze_anyframe_type(inst);
              i+=1;
              continue;
              },
              .array_cat=> {
              map[i] =analyze_array_cat(inst);
              i+=1;
              continue;
              },
              .array_mul=> {
              map[i] =analyze_array_mul(inst);
              i+=1;
              continue;
              },
              }
              break;
              }
              }
              externfnanalyze_add(inst: Inst) u32;
              externfnanalyze_addwrap(inst: Inst) u32;
              externfnanalyze_alloc(inst: Inst) u32;
              externfnanalyze_alloc_mut(inst: Inst) u32;
              externfnanalyze_alloc_inferred(inst: Inst) u32;
              externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
              externfnanalyze_anyframe_type(inst: Inst) u32;
              externfnanalyze_array_cat(inst: Inst) u32;
              externfnanalyze_array_mul(inst: Inst) u32;

              Two example prongs from the machine code:

              .LBB0_2:movedi,1call analyze_addmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0].LBB0_3:movedi,2call analyze_addwrapmov dword ptr [r14+rbx],eaxaddrbx,4moveax, dword ptr [r15+rbx]jmp qword ptr [8*rax+ .LJTI0_0]

              It's perfect! This is exactly what we wanted.

              This compromises the entire proposal. I will still post the proposal but this new discovery makes it seem unnecessary, since, in fact, we are hereby observing #2162 already implemented and working inside LLVM.

              Real Actual Use Case

              Here's one in the self-hosted compiler:

              switch (old_inst.tag) {

              This switch is inside a loop over ZIR instructions. In optimized builds, we noticed non-trivial amount of time spent in the overhead of this dispatch, when analyzing a recursive comptime fibonacci function call.

              This pattern also exists in:

              • The tokenizer
              • The parser
              • astgen
              • sema (this is the linked one above)
              • codegen
              • translate-c
              • zig fmt

              (pretty much in every stage of the pipeline)

              Other Possible Solution: Tail Calls

              Tail calls solve this problem. Each switch prong would return foo() (tail call) and foo() at the end of its business would inline call a function which would do the switch and then tail call the next prong.

              This is reasonable in the sense that it is doable right now; however there are some problems:

              • As far as I understand, tail calls don't work on some architectures.
                • (what are these? does anybody know?)
              • I'm also concerned about trying to debug when doing dispatch with tail calls.
              • It forces you to organize your logic into functions. That's another jump that
                maybe you did not want in your hot path.

              Proposal

              I propose to add continue :label expression syntax, and the ability to label switch expressions. Here is an example:

              constInst=externstruct {
              tag: Tag,
              constTag=externenum {
              end,
              add,
              addwrap,
              alloc,
              alloc_mut,
              alloc_inferred,
              alloc_inferred_mut,
              anyframe_type,
              array_cat,
              array_mul,
              };
              };
              exportfnentry(inst_list: [*]constInst, map: [*]u32) void {
              vari: usize=0;
              sw: switch (inst_list[i].tag) {
              .end=>return,
              .add=> {
              map[i] =analyze_add(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .addwrap=> {
              map[i] =analyze_addwrap(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .alloc=> {
              map[i] =analyze_alloc(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .alloc_mut=> {
              map[i] =analyze_alloc_mut(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .alloc_inferred=> {
              map[i] =analyze_alloc_inferred(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .alloc_inferred_mut=> {
              map[i] =analyze_alloc_inferred_mut(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .anyframe_type=> {
              map[i] =analyze_anyframe_type(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .array_cat=> {
              map[i] =analyze_array_cat(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              .array_mul=> {
              map[i] =analyze_array_mul(inst_list[i]);
              i+=1;
              continue :swinst_list[i].tag;
              },
              }
              }
              externfnanalyze_add(inst: Inst) u32;
              externfnanalyze_addwrap(inst: Inst) u32;
              externfnanalyze_alloc(inst: Inst) u32;
              externfnanalyze_alloc_mut(inst: Inst) u32;
              externfnanalyze_alloc_inferred(inst: Inst) u32;
              externfnanalyze_alloc_inferred_mut(inst: Inst) u32;
              externfnanalyze_anyframe_type(inst: Inst) u32;
              externfnanalyze_array_cat(inst: Inst) u32;
              externfnanalyze_array_mul(inst: Inst) u32;

              The new labeled continue syntax is syntactically unambiguous at a glance that it jumps to a switch expression, because it is the only form where continue accepts an operand. More details:

              • labeled continue with an operand on a loop would be a compile error
              • labeled break with a switch would be OK.

              How to Lower this to LLVM

              Note: I wrote this section before the EDIT: New Discovery section.

              One idea I had was to put the switchbr instruction inside each prong. I did some LLVM IR surgery to try out this idea (godbolt link):

              SwitchProngAdd: ; preds = %WhileBody%9 = loadi64, i64*%i, align8%10 = loadi32*, i32**%map, align8%11 = getelementptrinboundsi32, i32*%10, i64%9%12 = bitcast%Inst*%insttoi32*%13 = loadi32, i32*%12, align4%14 = calli32@analyze_add(i32%13)
              storei32%14, i32*%11, align4%15 = loadi64, i64*%i, align8%16 = addnuwi64%15, 1storei64%16, i64*%i, align8%17 = loadi64, i64*%i, align8%18 = load%Inst*, %Inst**%inst_list, align8%19 = getelementptrinbounds%Inst, %Inst*%18, i64%17%20 = getelementptrinbounds%Inst, %Inst*%19, i320, i320%a20 = loadi32, i32*%20, align4switchi32%a20, label%SwitchElse18 [
              i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
              ]
              SwitchProngAddWrap: ; preds = %WhileBody%21 = loadi64, i64*%i, align8%22 = loadi32*, i32**%map, align8%23 = getelementptrinboundsi32, i32*%22, i64%21%24 = bitcast%Inst*%insttoi32*%25 = loadi32, i32*%24, align4%26 = calli32@analyze_addwrap(i32%25)
              storei32%26, i32*%23, align4%27 = loadi64, i64*%i, align8%28 = addnuwi64%27, 1storei64%28, i64*%i, align8%29 = loadi64, i64*%i, align8%30 = load%Inst*, %Inst**%inst_list, align8%31 = getelementptrinbounds%Inst, %Inst*%30, i64%29%32 = getelementptrinbounds%Inst, %Inst*%31, i320, i320%a32 = loadi32, i32*%32, align4switchi32%a32, label%SwitchElse18 [
              i320, label%SwitchProngEndi321, label%SwitchProngAddi322, label%SwitchProngAddWrapi323, label%SwitchProngAlloci324, label%SwitchProngAllocMuti325, label%SwitchProngAllocInferredi326, label%SwitchProngAllocInferredMuti327, label%SwitchProngAnyframeTypei328, label%SwitchProngArrayCati329, label%SwitchProngArrayMul
              ]

              The machine code for the prongs looks like this:

              <snip>.LBB0_8: # %SwitchProngAnyframeTypemovedi,r12dcall analyze_anyframe_typemov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_7].LBB0_9: # %SwitchProngArrayCatmovedi,r12dcall analyze_array_catmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_8].LBB0_10: # %SwitchProngArrayMulmovedi,r12dcall analyze_array_mulmov dword ptr [r14+4*rbx],eaxmoveax, dword ptr [r15+4*rbx+4]addrbx,1jmp qword ptr [8*rax+ .LJTI0_9]<snip>

              Pretty nice. This is exactly what we want - there is an indirect jump in each prong directly to the next prong. But the problem is that even though we should have the same jump table 9 times, LLVM duplicates the jump table 9 times:

              .LJTI0_0: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10.LJTI0_1: .quad .LBB0_1 .quad .LBB0_2 .quad .LBB0_3 .quad .LBB0_4 .quad .LBB0_5 .quad .LBB0_6 .quad .LBB0_7 .quad .LBB0_8 .quad .LBB0_9 .quad .LBB0_10<snip>

              The duplicated jump tables are problematic, because in reality there could reasonably be about 150-200 instruction tags, which makes the jump table 600-800 bytes. This is fine; for example my L1 cache size is 256 KiB. But I wouldn't want to multiply that jump table by 200! It would be 156 KiB just for the jump tables alone. That would wreak havoc on the cache.

              Unless this improves upstream, the best strategy to lower this language feature will be for Zig to manually create the jump table itself instead of relying on LLVM to do it, using LLVM's ability to take the address of basic blocks and put them into an array. This will essentially generate the same code that you would get in Clang if you used computed goto in the traditional way.

              How to Lower this in Self-Hosted Backends

              We have lots of options here. It would be quite straightforward, since we have full control over AIR, as well as the backend code generation.

              OK But Is The Perf Actually Good?

              I don't know. I think realistically in order to benchmark this and find out if the machine code performs better we have to implement it first.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                acceptedThis proposal is planned.proposalThis issue suggests language modifications. If it also has the "accepted" label then it is planned.

                Type

                No type

                Projects

                No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions