The program I wrote uses stackalloc to allocate small memory (8byte~64byte), but unexpectedly found that some methods will read non-zero memory under the Release. by reading such related questions , I know stackalloc It is not always possible to allocate zero-initialized data.
localsinit has always been the default behavior of c#, and SkipLocalsInit can be used in c#9.0, but at present, what can I do to ensure that my stackalloc is forced to zero initialization?
If you're stackallocing into a span (which you absolutely should) it's just a matter of clearing immediately after creating:
Span<byte> buffer = stackalloc byte[8];
buffer.Clear();
I don't think it's possible to get a non-zero stackalloc'd memory after https://github.com/dotnet/roslyn/pull/39612 was merged.
As far as I understand, Roslyn could omit .locals init in some cases but the behavior was changed to always emit it for stackalloc and omit it only if [SkipLocalsInit] is specified (there is also a ILLink substep to drop all .locals init but it's not enabled by default).
If you're
stackallocing into a span (which you absolutely should) it's just a matter of clearing immediately after creating:Span<byte> buffer = stackalloc byte[8]; buffer.Clear();
You cannot guarantee whether the logic of manually performing cleanup and the logic of LocalsInit overlap, otherwise it will incur extra overhead. Therefore, what I want in the end is that the user actively controls the Init behavior (always force cleanup/skip init)
If you have a repro case where stackalloc does not zero memory please attach it to this issue
stackalloc should always zero init its allocated memory
stackalloc should always zero init its allocated memory
Only if .locals init is specified. And there were some cases where the C# compiler wasn't emitting it. My understanding is those were fixed, but it's possible there are still some remaining (beyond the desired cases where [SkipLocalsInit] is specified).
there is also a ILLink substep to drop all .locals init
FWIW, this was removed from the linker. It was a kludge before C# added SkipLocalsInit - the presence or absence of the flag should be decided by the author of the library, not at trimming time.
If you have a repro case where stackalloc does not zero memory please attach it to this issue
stackalloc should always zero init its allocated memory
my code will only show this unzeroed exception in my program logic. I tried to port my code to a clean test environment, but found that the call result was normal.
The following code has been collated:
`
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ulong ReadRaw64(ref int remaining)
{
ulong val;
if (remaining > 8)
{
val = Unsafe.ReadUnaligned<ulong>(ref _reader.GetRef());
_reader.Seek(8);
remaining -= 8;
}
else
{
ref byte refb = ref _reader.GetRef();
byte* p = stackalloc byte[8];
for (int i = 0; i < remaining; i++)
{
*(p + i) = ...;//irrelevant assign logic
}
_reader.Seek(remaining);
val = Unsafe.ReadUnaligned<ulong>(p);
remaining = 0;
}
return val;
}
`
If I change MethodImplOptions to NoInlining or NoOptimization, the expected result is normal. If MethodImplOptions is AggressiveInlining, it is not zeroed.
In this code, the local variable is 8 bytes, and my stackalloc is also 8 bytes.I guess it is because the compiler has made some optimizations, so in the second branch, the compiler directly uses local variables to replace the first address allocated locally.
However, then I tried to initialize the local variables again, but in the end it was still unzeroed,so I guess the compiler predicts that both branches will produce results on local variables, so in some cases it ignores the initialization behavior of the value.
I tried to port my code to a clean test environment
Was it exactly exactly the same environment (same runtime)? It appears that the bug (if there is one) is related to inlining. Could you show the caller? Does it reproduce with a fake reader (one where _reader.GetRef() always returns some static ref)? Does it reproduce in Debug?
Now, I have moved the test out and successfully restored the scenario I described above.
You can run the following code, different results will appear under debug and release.
```c#
public struct Reader
{
private byte[] _buffer;
private int _pos;
public Reader(byte[] buffer, int pos)
{
_buffer = buffer;
_pos = pos;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ulong ReadRaw64(ref int remaining)
{
ulong val = 0;
if (remaining > 8)
{
val = Unsafe.ReadUnaligned<ulong>(ref _buffer[_pos]);
Seek(8);
remaining -= 8;
}
else
{
ref byte refb = ref _buffer[_pos];
byte* p = stackalloc byte[8];
for (int i = 0; i < remaining; i++)
{
*(p + i) = Unsafe.Add(ref refb, i);
}
Seek(remaining);
val = Unsafe.ReadUnaligned<ulong>(p);
remaining = 0;
}
return val;
}
public void Seek(int pos)
{
_pos += pos;
}
public int ReadInt()
{
_pos += 5;
if (_pos == 5)
return 4;
return 3;
}
}
```c#
[Fact]
public void Test()
{
var testData = new byte[] { 193, 254, 20, 0, 0, 0, 2, 143, 4, 78, 97, 109, 101, 143, 1, 97, 143, 3, 65, 103, 101, 133, 10, 0, 0, 0 };
var reader = new Reader(testData, 0);
for (int i = 0; i < 2; i++)
{
int num = reader.ReadInt();
ulong val = reader.ReadRaw64(ref num);
Console.WriteLine(val.ToString());
}
//Output
//Debug:
// 76481024
// 9396481
//Release:
// 76481024
// 76505345
}
Simplified repro:
using System;
class Program
{
static void Main()
{
for (int i = 1; i >= 0; i--)
Console.WriteLine(Test(i)); // Test is inlined
}
public static unsafe byte Test(int i)
{
byte* p = stackalloc byte[8];
p[i] = 42;
return p[1];
}
}
net5.0-Release:
42
42
net5.0-Debug:
42
0
cc @briansull
I guess when stackalloc is inlined into a loop - it's not cleared every iteration.
Suspect fgInlinePrependStatements needs to add zeroing for the temps introduced when small stackallocs are converted to locals. We currently don't keep track of these, so doing this will require a bit more bookkeeping.
I've relabeled this from _tenet-performance_ to _bug_ since per @1996v and @EgorBo's repros (thanks both!) it seems like a legitimate missed edge case in the JIT rather than a performance defect.
@AndyAyersMS Is there a possibility this will need to be backported to previous releases? I can repro it on 2.1, 3.1, and 5.0, but I cannot repro it on Full Framework. (Perhaps methods with _localloc_ instructions aren't candidates for inlining on Full Framework?)
Seems like a possible candidate for backporting. The change that enabled this was dotnet/coreclr#14623, so predates 2.1.
As you note, the full framework jits will not inline methods with localloc.
I'm not clear why this is a JIT issue. Egor's repro shows that the C# compiler isn't emitting locals init for Test:
https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQxASwDYB8ACAmARgFgAoU/AAhwIHZSBvUi5qggNioBYKBZCNAHYAKAJRMWjEi2kUAZgHsEFIYJgU0FALwUCAbnUUAfNoAM+tAFoLYqTLvUAnEIAqcWCpEj9Aem8VXsOpQ6gIYgnAAJuLMAL6k0VQAzKwcAK4CUBCycBTAAJ7w/m4wKgJqaDbSknbM+fAAVBQADloUsBAAxgDWEBgY8h25BXAA2gAcALq6CdJNI2gTrZx407b2NM0jBFMJcSQxQA
.method public hidebysig static
uint8 Test (
int32 i
) cil managed
{
// Method begins at RVA 0x2074
// Code size 14 (0xe)
.maxstack 3
IL_0000: ldc.i4.8
IL_0001: conv.u
IL_0002: localloc
IL_0004: dup
IL_0005: ldarg.0
IL_0006: add
IL_0007: ldc.i4.s 42
IL_0009: stind.i1
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: ldind.u1
IL_000d: ret
} // end of method Program::Test
Maybe there's a JIT issue in addition to this, but I would think with this particular repro, the JIT is doing the "right thing"?
@stephentoub Derp, you're right. 馃お I forgot the compiler treated byte* ptr = stackalloc ...; and Span<byte> span = stackalloc ...; differently.
Edit: I guess it's not just byte* vs. Span<byte>. It's also that there aren't any locals at all in the _Test_ routine.
I think there is a jit issue here too. Looking at the logic, we probably should not be inlining this method at all, which is why the inlining code doesn't handle this case. That's sort of good news for possible back-porting because blocking the inline is much simpler than allowing the inline and then trying to fix things up properly after inlining.
The current detection for localloc in loop doesn't kick in if the localloc is in a callee and the loop is in the caller; the the check is
(block->bbFlags & BBF_BACKWARD_JUMP) == 0)
and it needs to be more like
((block->bbFlags & BBF_BACKWARD_JUMP) != 0)
||
(compIsForInlining() && ((impInlineInfo->iciBlock->bbFlags & BBF_BACKWARD_JUMP) != 0))
This version also has the bug...
```C#
public static unsafe byte Test(int i)
{
int j = i;
byte* p = stackalloc byte[8];
p[j] = 42;
return p[1];
}
with IL
.method public hidebysig static uint8 Test(int32 i) cil managed
{
// Code size 16 (0x10)
.maxstack 3
.locals init (int32 V_0)
IL_0000: ldarg
...
```
For the original version, could it be that ILDASM doesn't bother to show the bit if there are no locals? (that is it might be set but just not showing up?)
@briansull I have a fix, want me to take this one over?
Yes, You can take it over Andy
Most helpful comment
Now, I have moved the test out and successfully restored the scenario I described above.
You can run the following code, different results will appear under debug and release.
```c#
public struct Reader
{
private byte[] _buffer;
private int _pos;
}