microsoft / microsoft/DirectXShaderCompiler
[SPIR-V] Nested static 'for' loops with unroll translate to 'while( true )' loops with actual branches
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 3.7k
- Forks
- 900
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 44
Description
Hi! We're using DXC version 1.6.2104.52. We target mobile devices, such as Adreno and Mali GPUs.
Consider the following code:
float4 color = (float4)0;
float4 colors[4];
float luminances[4];
[unroll]
for ( int j = 0; j < (4 - 1); j++ )
{
[unroll]
for ( int k = 0; k < (4 - j - 1); k++ )
{
[flatten]
if ( luminances[k] > luminances[k + 1] )
{
float tmpLum = luminances[k];
luminances[k] = luminances[k + 1];
luminances[k + 1] = tmpLum;
float4 tmpColor = colors[k];
colors[k] = colors[k + 1];
colors[k + 1] = tmpColor;
}
}
}
All of the iteration bounds are known at compile time, so we expect this loop to be fully unrolled. But, when viewing shader disassembly, the final code looks like
while(true) {
int _117 = Phi(0, _118);
bool _120 = _117 < 3;
[[Unroll]]
if(!_120) break;
while(true) {
int _124 = Phi(0, _125);
int _127 = 3 - _117;
bool _128 = _124 < _127;
[[Unroll]]
if(!_128) break;
float* _131 = &_74[_124];
float _132 = *_131;
int _125 = _124 + 1;
float* _133 = &_74[_125];
float _134 = *_133;
bool _135 = _132 > _134;
[[Flatten]]
if(_135) {
float _138 = *_131;
float _139 = *_133;
*_131 = _139;
*_133 = _138;
float4* _140 = &_73[_124];
float4 _141 = *_140;
float4* _142 = &_73[_125];
float4 _143 = *_142;
*_140 = _143;
*_142 = _141;
}
}
int _118 = _117 + 1;
}
When doing Mali GPU captures, this produces actual branches on GPU with warp divergence up to 30%. Running SPIR-V binaries through malioc shows that the resulting shader is disproportionally bottlenecked by Load/Store unit operations (1.8 cycles arithmetics, 15 cycles worst case Load/Store unit operations).
If we unroll the shader manually, like this
void boxMedianOneStep( int j, inout float4 colors[4], inout float luminances[4] )
{
[unroll]
for ( int k = 0; k < (4 - j - 1); k++ )
{
[flatten]
if ( luminances[k] > luminances[k + 1] )
{
float tmpLum = luminances[k];
luminances[k] = luminances[k + 1];
luminances[k + 1] = tmpLum;
float4 tmpColor = colors[k];
colors[k] = colors[k + 1];
colors[k + 1] = tmpColor;
}
}
}
boxMedianOneStep( 0, colors, luminances );
boxMedianOneStep( 1, colors, luminances );
boxMedianOneStep( 2, colors, luminances );
the resulting code is perfectly flat, L/S bottlenecks are gone and there is zero warp divergence. The unrolled shader is up to 8x faster than the original one. Our legacy GLES toolchain (FXC->HLSLcc) handles this case with no problem. The new DXC/SPV-Opt toolchain does not, and even running additional loop unroll passes on final SPV code does not help at all.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the supplied nested HLSL reproducer and compare its generated SPIR-V disassembly with the manually unrolled version. Run the example through DXC 1.6.2104.52 and inspect the loop-unrolling and SPIR-V lowering behavior; done means compile-time-bounded nested loops produce flat code without the shown loop branches or divergence.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100