Maybe I’m missing something, but this blog post seems flat our wrong to me?
It replaces a conditional with a lerp and then claims it’s better now?
The problem with conditionals like that, is that people expected the code in them to only be executed if the conditions are fulfilled, but GPUs tend to execute everything anyway and just discard the result of the don’t end up using it.
So the actual problem is if you were hiding expensive functions inside conditionals and you were expecting that they would not be evaluated for every pixel then nope, you were wrong. Somehow people came to the conclusion that conditionals are expensive, which is the wrong conclusion.
I might be completely wrong, so please do not trust too much what I say, but so far my understanding of branching is that it all depends on the context, for example if I use constant like this:
void SomeFunction(float Predicate, float4 TrueValue, float4 FalseValue, out float4 Out)
{
Out = Predicate ? TrueValue : FalseValue;
}
// [...] Somewhere in the code
SomeFunction(1, 0.5, 0.8, outVar);
Compiler knows it’s always true and there will be no “if” at all in compiled code, but if predicate came from uniform variable it is not possible to determine the result during compilation, however it is uniform for all pipes, so it must be checked only once and this is why such simple if (aka static branch?) is super fast. As @AcidArrow said both sides are going to be executed anyway and if I am correct in HLSL ternary operator enforces that, and branch is never dynamic.
There is also dynamic braching and it happens when you test dynamic value that can be different per pixel, for example you sample texture or world space position. From what I know If you had huge chunk of code inside branch it could make sense (it will try to process one branch and it will be still worth after reverting), but often that is not the case. I think you can enforce types of branch in HLSL with [branch] or [flatten], however I have no knowledge how compiler picks type of branch without hint (I know it’s flatten by default).
My guess is that short if can be optimized by compiler and it’s better to let him do the work or they changed lerp to ‘if’ as it makes sense - the node name is “Branch”, not “Lerp”.
Unfortunately both of those links are flat out wrong.
Quick bullet points:
Branches on GPUs are not a huge performance problem when used smartly on modern GPUs.
A conditional statement like an if or ? in a shader does not mean it’ll be a branch! Shader compilers will try to “smartly” choose whether to use a branch or a swap based on the code.
Most conditionals will end up not being a branch because the compiler won’t choose to make it one in most cases.
Using a lerp(b, a, step(y, x)) is always slower than (x >= y ? a : b) because the step is already doing (x >= y ? 1 : 0) so you’re just adding two more instructions to do a lerp that could be skipped.
Any conditional can become a branch if the shader compiler decides to make it one! That includesstep because it’s a conditional statement! (Though this is highly unlikely.)
A discard or clip() is also a branch!
Let’s focus on the xdpixel link since the exiin link is based on the information from that. That post talks about branch prediction and how missed predictions are bad for CPUs performance due to pipeline stalls. This is true.* Then it goes to talk about how on GPUs it’s not really an issue of missed prediction, but rather potentially bad utilizations of the ALU if more than one branch path needed to run at the same time. This is also true, but we’ll come back to that in a moment.
Though it ignores the fact it’s not really a major problem on modern CPUs as the pipeline is relatively short and memory access is almost alway the limiting performance factor for modern PCs.
The example he then gives is using the stereo eye index to branch between two possible color values and goes “see, branch bad!”
There’s a big problem with that conclusion. In that example there will never be more than one branch ever executing at one time. It is the perfect use case for using branching on a GPU. Indeed the whole reason why the compiler chose to use a branch there is because it knows it’s a good use case.
The big thing missing in the discussion about GPUs and ALU utilization in branches is what the phrase “at the same time” means. In the example given, it’d be a SIMD core with 8 ALU threads. On modern GPUs it’s more like 32 or 64 ALU threads which different GPU manufacturers refer to as “waves” or “warps”. When rendering pixels, each warp is rendering an 8x4 or 8x8 square of pixels on screen, and only on a single triangle at a time. If the value the branch is dependent on does not change across the entire triangle, or just within that group of pixels, then there’s perfect utilization of those ALU threads!
In the example case of the stereo eye index, that is a value coming from the GPU that is guaranteed to be constant for the entire triangle. So the branch will always only ever take one path for all threads in the group. It should be a branch!
Other types of values that are great for branching on:
Material properties
Instance ID or Instanced properties
Primitive ID
Values passed from the vertex to the fragment using a nointerpolation modifier.
All of those will be constant across the entire triangle when rendering the fragment shader, so there will never be any issue with both branches running. GPU can guarantee that ahead of time.
That all said, it’s still not necessarily a bad thing to use a real branch in other cases where you know the value will often be consistent for many of those “warp” groups. For example branching on a texture mask or regular interpolated value passed to the fragment shader. If it means avoiding some expensive calculations for a good portion of the screen, then great. And think about it this way: if you don’t use a branch you’re just doing the expensive calculation all of the time anyway.
Modern GPUs are also just shockingly fast these days, even on mobile. They can do an amazing amount of math without any problem. Memory bandwidth is often the bigger limiting factor. So things like using a lot of interpolators to pass data between the vertex and fragment shader can be much slower than recalculating the same data in the fragment shader, or sampling from a lot of textures, or a lot of random positions within a single texture, etc. can be the thing that makes things slow. Your shader using a branch or not probably isn’t going to be the factor that is limiting your performance.
This was especially true in the days before Direct3D 10.0 and OpenGLES 3.1 where a lot of GPUs did not actually support branches at all! All code was always run no matter what, so like @AcidArrow described people would write an if statement and get terrible performance and then blame the if statement not realizing it didn’t actually do a branch at all. But, as mentioned above, this can still be the case today as most of the time the GPU will not actually end up branching because the shader compiler won’t compile conditionals as a branch the majority of the time. And you end up with the same problem of “I used an if statement and it’s slow, so it must be the if statement’s fault” misdiagnosis.
If you’re branching based on hardcoded values within the shader code itself, those will never end up in the compiled shader as a branch because the shader compiler will calculate the result and use that value instead. But compiler code stripping is kind of a different topic.
If you read that twitter thread you’ll see I was wrong about that. A ternary ((x > y ? a : b)) can end up being a branch if the shader compiler decides to make it one. It’s just very rare because most of the time the compiler won’t make any conditional a branch unless you explicitly ask it to make one or you’re testing against a value that it knowns will be constant.
To answer a specific question in the original post
All of Unity’s Shader Graph documentation has that “possible outcome” phrase. It seems to be used as a catch all for cases where the code generation might have multiple options, or for cases where the version of Shader Graph you’re using and the version of the documentation you’re looking at don’t match. But in the case of the Branch node it can be trusted 100% to be those outcomes for those versions of Shader Graph since that’s the only code they generate.
Other nodes often have a lot more code snippets, or call a function in an external file that might have different functions depending on the platform.
Also they changed those nodes because I and several others complained about the Branch node being a lerp instead of a ternary, and Unity verified internally that it was slower and changed it. The funny thing being, as discussed above, it probably won’t actually be a branch!
I am always amazed at how extensive your answers are.
I want to make something clear - does it matter for compiler if tested value (in branch) comes from consistent source? I mean, do I pay cost only when it comes to this situation when different branches are taken or compiler optimizes it differently (structure). Is branch like this if (materialProp > 0) ... exactly the same type as if (randomVal > 0) ..., but the second one is bad only becuase branch result will be different very often by definition?
Yeah, I meant code stripping here, however I would like to ask something when it comes to this topic.
Here is the code from URP Lit shader (source link):
There is constant “variable” specularHighlightsOff passed to this function based on _SPECULARHIGHLIGHTS_OFF keyword, what is the reason to create branch here if result will be always the same, does it serve some special purpose or it is just mistake? Is compiler going to strip this anyway?
If it decides to compile as a branch, and the branch is coming from a source the compiler can guarantee is constant per warp, it’s a “fast” dynamic branch. If it can’t be guaranteed to be constant, then it’s a “slow” dynamic branch. This distinction seems to matter more on mobile and AMD GPUs than it does on Nvidia GPUs.
(edit: To clarify “fast” dynamic branches are basically free on desktop GPUs, where “slow” dynamic branches will cost a few cycles in overhead.)
There are complicated ways in some of the latest graphics APIs to tell the GPU the value is going to be constant per warp even if it’s not coming from a source that normally is that I’ve seen discussed for use with clustered & tiled lighting setups to force fast branches. But I’m honestly not up to speed enough with those to explain how that works apart from at the very high level “that’s what it’s doing”.
The answer is yes, it will strip that code, except when it doesn’t.
There are multiple declarations of the LightingPhysicallyBased function in that file. Some of which take a specularHighlightsOff and some that don’t. In the ones that don’t, the value is hardcoded based on the define and then it calls the function that does take that as an input. In that case the if statement and the preceding [branch] will be stripped. However if the shader code is directly calling the function with the bool, and that bool isn’t hardcoded, then it’ll try to be a branch.
Another wrench to throw into all of this is the “compiled shader” isn’t the final form. Even if the “compiled shader” has a branch in it, that shader gets “compiled” again when the drivers take it and convert it to assembly code for the current GPU hardware. That can also make decisions to make something a branch, or not.
I can’t express how blessed i feel while reading detailed and sharp answers from bgolus. Thank you sir.
found another good explanation here.
about “DFC best used on spatially-coherent branches”
an interesting post for visualizing branch and sample counts.
One of highlight for me is it’s okay if same branch is being used for that entire triangle.
What can be passed from Vertex Shader that is consistent for all three vertices of triangle,
So that after interpolation, the value is still above/below a certain threshold to have a common branch in fragment shader.
Or Compiler won’t understand what i am trying to do, and the output will be very different ?
Hey, I just encounter a weird issue that in a reproduce case in HLSL in renderdoc and simple logic looks like below:
[branch]
if(View.HasLight){
// do something, But may has NaN or Inf or precision issue
}
I tried to fix artifact like half pixels on a model is totally wrong from some directions. We fix some obvious inpropriate code in the dynamic branch and got the right result. But the problem the ‘View.HasLight = 0’ and debug in renderdoc can verify that step can successfully skip it.
Just wondering is there any strategies for hlsl compiler that affects in this case the branch is stripped but there has bug inside it?
Assuming the various material properties are never written to by the shader code, these can all be handled as “fast” dynamic branches as the values will be constant across the entire draw.
Again, whether or not they will be depends on the compiler, but they can be.
There are also some interesting things in this talk from Jason Booth
(I was also surprised that Chat GPT knew about a lot of intricate GPU architecture details I would have never expected due to their extreme niche)
For Branches I do understand that it should be avoided heavily that the branch condition is any expensive, as it might become quickly more expensive than the savings
So I gather using things like branching by texture masks should be heavily avoided
And stuff like Normal Direction Y, or Vertex color, or Object position (Or of course simple statics) are viable to branch and remove expensive areas on the material when not needed ?