← 输入框|github.com|刷新
Skip to content

Should there be a way to prevent loop unrolling? #4110

Open

Description

@myth0genesis

Firstly, apologies for posting this issue here if this happens to be the wrong place to do it. I recently finished writing a lengthy semi-recursive fragment shader that takes quite a while to compile. Coming from GLSL, the simple fix (at least for ANGLE) is to change the initializer in a for loop to include a uniform that varies from frame to frame. I tried the same thing in WGSL to see if it would prevent the compiler from unrolling a for loop by changing

for (var i: i32 = 0; i < maxstps; i++) {
...

to this:

for (var i: i32 = min(0, i32(#some_varying_integer#)); i < maxstps; i++) {
...

In ANGLE, this usually fixes the issue and can cut minute-plus compile times down to just a fraction of a second, but I was disappointed to see that this doesn't work for Tint. The other way to fix this would be with a preprocessor directive, but I see there is no preprocessor for WGSL that could handle something like this. It might seem like this would address a small number of cases, but there's certainly no shortage of shaders on Shadertoy that absolutely need to prevent loops from unrolling because they have many or lengthy loops in them.

Activity

  1. ben-clayton commented on May 15, 2023

    @ben-clayton
    Contributor

    Firstly, apologies for posting this issue here if this happens to be the wrong place to do it.

    The best place to file tint implementation issues is at crbug.com/tint - however compiler hints to roll or unroll loops is an interesting feature request, and this would be the correct place to file that request.

    The other way to fix this would be with a preprocessor directive, but I see there is no preprocessor for WGSL that could handle something like this.

    WGSL has attributes which would be the logical way to annotate a loop with a roll / unroll hint. Note that unrolling is usually easier to guarantee than preserving loops, so a @roll style attribute might still unroll the loop on some backends.

    It might seem like this would address a small number of cases, but there's certainly no shortage of shaders on Shadertoy that absolutely need to prevent loops from unrolling because they have many or lengthy loops in them.

    Some might argue that if they're needed, then the compiler shouldn't have attempted to unroll the loop in the first place.
    Unfortunately, the issue you've reported with Tint sounds similar to some other issues reported recently. These have been issues with the backend HLSL compiler. Tint is hoping to move to a more modern HLSL compiler later in the year, but a small minority of users will still require the old code path.

  2. myth0genesis commented on May 15, 2023

    @myth0genesis
    Author

    Some might argue that if they're needed, then the compiler shouldn't have attempted to unroll the loop in the first place.

    I see your point here. It's actually more of a convenience thing. The main reason shader programmers like to preserve loops is for quick iteration. Having a 30-second-plus compile time for a shader can easily turn a 5-minute process into an hour-long one if you need to make a bunch of small changes and visually inspect the results before finalizing with a more optimized version.

  3. litherum commented on May 16, 2023

    @litherum
    Contributor

    What would such an annotation be translated to in MSL, SPIR-V, and HLSL?

  4. ben-clayton commented on May 16, 2023

    @ben-clayton
    Contributor
  5. litherum commented on May 18, 2023

    @litherum
    Contributor

    SPIR-V says:

    Strong request, to the extent possible, to keep this loop as a loop, without unrolling.

    Given that it may not do anything in MSL, and may not do anything in SPIR-V, we'd either have to make the spec requirements loose, or just not spec this at all.

    I'm also not sure that this is testable.

  6. myth0genesis commented on May 19, 2023

    @myth0genesis
    Author

    You could write a shader with a large loop that would greatly increase compile times when unrolling the loop and compare compile times between different builds. I guess it would be more of a qualitative way to test, but it would be easy to tell the difference. I can see why there would be reluctance to spec this given the platform variation, though. I was just reading about another way one can trick ANGLE into preventing a loop unroll. Haven't tested if it would translate to WGSL, but it might be worth a look to see if there are ways one can manually coerce the compiler into preventing a loop unroll in order to establish a "goal" result.

  7. ben-clayton commented on May 19, 2023

    @ben-clayton
    Contributor

    Given that it may not do anything in MSL, and may not do anything in SPIR-V, we'd either have to make the spec requirements loose, or just not spec this at all.

    As I said above, they might not do anything in HLSL either. They're just hints to the compiler. You would need some spec for this in order for the attribute to not be rejected.

    I'm also not sure that this is testable

    Sort of. Loop rolling or unrolling should not change the behaviour of the executing program, only (possibly) the runtime performance and compilation performance. You can't write CTS for the latter, so the tests for this would be:
    (a) the attributes aren't rejected by the compiler
    (b) the program behaves the same way with either attribute
    (c) you can't apply roll and unroll, as that doesn't make sense

    That's all you could test.

  8. kdashg commented on May 24, 2023

    @kdashg
    Contributor
    WGSL 2023-05-23 Minutes
    • KG: Two underlying backends that can consume this. Makes me inclined to pass it along.
    • JB: Like the criteria Ben suggested. Don’t specify the meaning. Doesn’t change the semantics.
    • BC: This can be something to massage the questionable behavior of FXC. But most of the time it doesn’t do anything. Might be more helpful on compilers that behave better with the additional control. The original issue filed isn’t a compelling argument for us. Trying to run away from FXC as quickly as we can. Hopefully won’t affect our users in future. Haven’t checked DXC’s response to this control. Definitely consumable in SPIR-V.
    • JB: If this is just a platform issue with minute long compiles. If us middleware can avoid we should just do that, rather than exposing controls to users.
    • MM: Compare to ‘register’ keyword in C. Compiler has a lot of insight into what should be unrolled. Among implementors in this group, do our implementations unroll at the WGSL level? Do loops always pass loops.
    • BC: Tint does not unroll.
    • JB: Naga does not unroll.
    • MM: An attribute that might not do anything in any backend (in at least some cases, but maybe many cases), seems like a “not-right-now” and maybe even “never.”
    • BC: Tint might in future do some.
    • KG: We keep loops intact and could pass this down.
    • AB: Generally not in favor of adding hints. I regard this as a hint. Don’t think there’s a ton of value to them. Generally your compiler should do “the right thing”. Odd use pragmas is not the right way to deal with those problems.
    • KG: context is this problem exists for a decade, at least. Standard solution is to trick the compiler into not being able to unroll the loop. SPIR-V seems the strongest argument for this. In practice the compiler might not be able to know how long it will take to unroll. Is a bit of a hack. Not sure a compiler can “just fix this”. People are getting use out of them.
    • MM: Avenue for compromise: wait until no implementations run on FXC, then see if there is still utility.
    • KG: would rather do the opposite.
    • JB: The more I listen, the more I think it’s our job to fix. The user doesn’t know the minute long compile is caused by loop unrolling. Who is in a better position to investigate and understand when fix needs to be applied. Think we should take it out of their hands. Don’t offer little switches.
    • DS: We’ll run into with CTS: there’s a set of loops that we unroll on metal but force unroll on FXC. One is slow in one and vice versa the other. Will we need to expose a feature that behaves differently based on underlying platform?
    • KG: Ideally we would fix it for the user. In practiced, the whole field has not solved the problem in all the time we’ve had so far. Think it’s too ambitious. Tolerable amount of cruft to have the feature; we can deprecate later with console logs. Feel relatively strongly we should have these annotations.
    • AB: Feel we’re in a different position now. Because we’re a layer removed. Don’t see why it has to bubble all the way up.
    • KG: My concern is we can’t tell which loops will blow up.
    • MM: Disagree with Kelsey but your point is valid. Underlying I think the argument is: how would we know at our layer whether to emit the platform unroll hint.
    • KG: Want us to be conscious that devs run into this problem for a very long time. If it were easy to fix by compilers, then compilers would have fixed it. That it’s still a problem today by multiple compilers, then it’s still hard.
    • JB: It’s not that other compilers blowup. It’s a pessimization on MSL. It’s really only FXC that we have a critical problem with.
    • KG: Have heard: we want to switch of FXC. Long term goal we might get to eventually. We think but can’t prove that you won’t need this in a post-FXC world. But post-FXC world is years away. On behalf of developers, help those developers now.
    • BC: When compilers respect this hint, it’s very useful in games development. Very useful in console games. Regardless of compiler bugs, if this is respected, it’s very useful to have.
    • MM: Counterargument to consoles is you’re only targeting one device. Same question to author: how does the author know whether to insert the hint.
    • MM: Ask Kelsey are you making a value judgment which is better:
        1. having two years of allowing authors to get perf workaround in FXC, but then making them no-ops
        1. Can’t write the hint, and have implementations fix it.
    • KG: Probably yes it’s worth it to have 2 years of workarounds. (But also have doubts we will fix it perfectly.)
    • MM: That’s a reasonable position to have. Coherent. I disagree with it tho.
    • KG: Along the risk spectrum, it still may be useful in 2years anyway. Think it’s a very light burden to hold.
    • JB: The cruft argument doesn’t bother me at all. My concern is it’s effective some platforms and wrong in other platforms. My guess is we could do a better job trying to handle it ourselves. It’s different than the last 10 years because we’re a new layer in a position to fix things.
    • MM: Think that makes sense. Compilers already have sophisticated logic on how /when to unroll. Making those complex decisions more sophisticated by adding compilation time as an input to the decision seems reasonable.
    • (no resolution)
  9. added
    wgsl resolvedResolved - waiting for a change to the WGSL specification
    on Jun 6, 2023
  10. added this to the post-V1 milestone on Jun 6, 2023
  11. kdashg commented on Jun 6, 2023

    @kdashg
    Contributor
    WGSL 2023-05-30 Minutes
    • MM: Thinking about Kelsey’s argument that folks get value out of it for a long time. I’m no longer on any side of the issue.
    • JB: Not objecting any more. Would have to object to “inline” as well.
    • KG: Consensus: wish we didn’t have to do this. Might be useful. If we had it, it’s not that costly, but unhappy to add it. Prefer to not add hints that we may often want to ignore.
    • AB: There’s HLSL support, and in SPIR-V. Is there anything in Metal? It’s firmly a hint because we can’t even translate it to one of your platforms. Is that a good candidate for being in a core lang. Feels even more wishywashy if can’t be applied everywhere.
    • KG:Fundamentally it is a hint. But people do have value from it.
    • BC: Even if it doesn’t exist in MSL, Tint is gradually morphing into an optimizing compiler. We could implement it inside Tint.
    • MM: Clang attributes do_unrolling does work in metal. Even if you write the attribute, doesn’t mean the backend compiler honours it.
    • AB: Makes me more comfortable that we can move the information along.
    • MM: Seems we’d want both directions.
    • KG: Feels “no_unroll” is a stronger need.
    • AB: Think we add both. Assume this is an attribute. Similar to diagnostic attribute in terms of where it can apply.
    • https://gpuweb.github.io/gpuweb/wgsl/#filterable-triggering-rules
    • @unroll for (...) { }
    • JB: Yes, reasonable to restrict to a subset of possible locations.
    • KG: Why is ‘either’ a mistake?
    • MM: I expect if you can put it in different places, it would be because it would imply that it does different things.
    • BC: In this case, the reason that both are available is that ‘before’ is that that would apply to e.g. the triple within the for () parens, and ‘after’ is just the block.
    • JP: Should we allow unroll(n)? What would happen if it’s not supported by a backend?
    • KG: E.g. impl might see unroll(2) and decide not to unroll, up to impl.
    • ??: We should maybe defer for now and proceed with just @unroll and @no_unroll?
    • Resolved: @unroll and @no_unroll (and not “@unroll(n)” for now - maybe later if necessary)
  12. SarkyKeshy commented on Jun 9, 2023

    @SarkyKeshy

    I have found that if a loop assigns data to a reference that might be accessed by a subsequent iteration of the loop it forces the loop to unroll. I assume it is because even if a loop is not unrolled the shader when running will attempt to run multiple iterations of the loop simultaneously. As long as there is no assigning to a shared variable this seems ok. Otherwise it unrolls. This is my experience using WebGL2.0 anyway.
    Having this works fine, but as soon as the comment is made into an assignment it crashes due to being unable to unroll.

    for (int h=min(0,shaderData.numFormats); h<shaderData.numFormats;h++) {
    // plus 12 means colors start at position 13 (index 12) in data floatArray.
    sFormatData format;
    float forCode = da(offset); //fStyle*64.0+fType
    float col0 = da(offset+1.0); //color.rg
    float col1 = da(offset+2.0); //color.bs

    	int forType=int(mod(forCode,32.0));
    	int forStyle=int(floor(mod(forCode/32.0,32.0)));
    	float texID=floor(forCode/2048.0);
    	format.forType = forType;
    	format.forStyle = forStyle;
    	format.texID = texID;
    	format.color.x = fract(floor(col0/2048.0)/256.0);
    	format.color.y = fract(col0/256.0);
    	format.color.z = fract(floor(col1/2048.0)/256.0);
    	format.color.w = fract(col1/256.0);
    	format.color = shaderData.formats[h].color * colAdjustment; // 255->1.0
    	offset+=3.0;
               //	shaderData.formats[h] = format;
    

    }

    If any one has a solution to this I would love to know.

  13. myth0genesis commented on Jun 11, 2023

    @myth0genesis
    Author

    @kdashg Thanks for pushing for this and keeping us up-to-date!

    @SarkyKeshy If you look at my first post, you'll see I mention the for (int i = min(0,... trick not working with Tint. No workaround that I'm aware of yet.

    Is it okay to close this topic or does it still need to be open for further consideration?

  14. FreddyFunk commented on Sep 4, 2023

    @FreddyFunk

    A loop unroll prevention is necessary to implement any kind of semi efficient raymarching renderer. What would be nessesary to implement such a statement like the previous suggested @roll? I could give it a shoot and try to implement it, since most backends support this anyway.

  15. Jasper-Bekkers commented on Sep 12, 2023

    @Jasper-Bekkers

    I just stumbled upon this issue after googling around for a way to specify how to flatten/unroll loops (rather then what this issue is about which is to force dynamicism in a loop). However, since both got addressed in the minutes I'll chime in a bit with some practical experience.

    1. Unroll / loop attributes as seen in other compilers are always seen as hints. The driver is free to ignore them, and can choose to respect the attributes in the end.
    2. Unroll and loop work alongside the optimizing compiler, but are not substitute for it, nor can they be substituted by it. The compiler often guesses right, but occasionally the heuristics in there are wrong and you need to override it's behavior.
    3. Both unroll and loop attributes are used quite often in practice. Use cases range from force-reducing control-flow divergence such as in the case mentioned before with the raymarcher (where you want [loop] potentially early out of loops). Another use-case (why I started googling) is to prevent memory spills with dynamic indexing for example.

    Given that compilers are fallable and that an optimizing compiler traditionally can't make much assumptions about the length of loops it occasionally happens that things get miscompiled.

    I'll add a practical example below using naga. We could consider this a bug in naga as well, but assuming this isn't an isolated event (miscompilation can happen anywhere in the chain, drivers, translators etc) it's interesting to bring up here as well.

    The following code would, under regular conditions, typically end up unrolled by the driver all the way to the end. The scenario is pretty simple here, PIXELS_PER_THREAD is 4, this loop indexes into a variable. Gpus don't typically allow for indexing into registers, so ultimately the data gets spilled to memory, causing performance hitches. To prevent this, compilers unroll this loop.

        for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
            rgba[i] = unpack4x8unorm(config.base_color).wzyx;
        }

    However, naga translates this into something similar to the following, which both DXC and the downstream amd compiler fail to pick up (https://godbolt.org/z/j9WsWxda5 for RDNA ISA, compared to with for-loop https://godbolt.org/z/z53rzv73e)

        i = 0u;
        bool loop_init_8 = true;
        while(true) {
            if (!loop_init_8) {
                uint _expr31 = i;
                i = (_expr31 + 1u);
            }
            loop_init_8 = false;
            uint _expr20 = i;
            if ((_expr20 < PIXELS_PER_THREAD)) {
            } else {
                break;
            }
            {
                uint _expr23 = i;
                uint _expr27 = config.base_color;
                rgba[_expr23] = (float4(_expr27 & 0xFF, _expr27 >> 8 & 0xFF, _expr27 >> 16 & 0xFF, _expr27 >> 24) / 255.0).wzyx;
            }
        }
  16. kdashg commented on Sep 13, 2023

    @kdashg
    Contributor

    Sorry that the resolution was not clear, but from the minutes:

    Resolved: @unroll and @no_unroll (and not “@unroll(n)” for now - maybe later if necessary)

    We're just not implementing it yet, based on the milestone for this issue.

  17. myth0genesis commented on Sep 13, 2023

    @myth0genesis
    Author

    Oh. Yeah, I was confused, but I'm an easily confused person 😅 Thanks for the clarification!

  18. pverlind commented on Sep 30, 2025

    @pverlind

    We are running into this in our application as well. We notice an up to 10x slower compile time on Windows for shaders compared to Linux/Mac in Chrome. Having some control over the unrolling would probably help us to close the performance gap.

  19. dj2 commented on Sep 30, 2025

    @dj2
    Member

    If you have the shader, it should be possible to write out the HLSL directly (using the tint or naga command line programs). You could then time the DXC compile without hints add the unroll hints into the HLSL shader and run it again to get an idea of the impact of the hints. Do they make it faster? 10x faster?

  20. kainino0x commented on Oct 1, 2025

    @kainino0x
    Contributor

    If on a Chromium browser, IIRC launching the browsers with --enable-dawn-features=dump_shaders,disable_symbol_renaming should also let you get the HLSL code easily.

  21. pverlind commented on Oct 1, 2025

    @pverlind

    I captured the HLSL output and measured the compile time with DXC. Loop unrolling is not the main issue for us it seems. I do notice that I get a 3 to 4x speed up when I disable optimisations for the compile. But I must admit I need to read up a bit on what the exact consequences of this are, and that's a different issue than this one all together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestA request for a new GPU feature exposed in the APIwgslWebGPU Shading Language Issueswgsl resolvedResolved - waiting for a change to the WGSL specification

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions