Ce code dangereux devrait-il également fonctionner dans .NET Core 3?

42

Je refactorise mes bibliothèques Span<T>pour éviter les allocations de tas si possible, mais comme je cible également des cadres plus anciens, j'implémente également des solutions de secours générales. Mais maintenant, j'ai trouvé un problème étrange et je ne sais pas trop si j'ai trouvé un bogue dans .NET Core 3 ou si je fais quelque chose d'illégal.

Le problème:

// This returns 1 as expected but cannot be used in older frameworks:
private static uint ReinterpretNew()
{
    Span<byte> bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return Unsafe.As<byte, uint>(ref bytes.GetPinnableReference());
}

// This returns garbage in .NET Core 3.0 with release build:
private static unsafe uint ReinterpretOld()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1; // FillBytes(bytes);

    // returning bytes as uint:
    return *(uint*)bytes;
}

Il est intéressant de noter que cela ReinterpretOldfonctionne bien dans .NET Framework et dans .NET Core 2.0 (donc je pourrais en être satisfait après tout), mais cela me dérange un peu.

Btw. ReinterpretOldpeut également être corrigé dans .NET Core 3.0 par une petite modification:

//return *(uint*)bytes;
uint* asUint = (uint*)bytes;
return *asUint;

Ma question:

Est-ce un bogue ou ne ReinterpretOldfonctionne- t-il que par accident dans les anciens frameworks et dois-je également appliquer le correctif pour eux?

Remarques:

  • La version de débogage fonctionne également dans .NET Core 3.0
  • J'ai essayé d'appliquer [MethodImpl(MethodImplOptions.NoInlining)]à ReinterpretOldmais il n'a eu aucun effet.
György Kőszeg
la source
2
FYI: return Unsafe.As<byte, uint>(ref bytes[0]);ou return MemoryMarshal.Cast<byte, uint>(bytes)[0];- pas besoin d'utiliser GetPinnableReference(); en regardant l'autre, cependant
Marc Gravell
SharpLab au cas où cela aiderait quelqu'un d'autre. Les deux versions qui évitent se Span<T>compilent en IL différent. Je ne pense pas que vous fassiez quelque chose d'invalide: je soupçonne un bogue JIT.
canton7
quelles sont les ordures que vous voyez? utilisez-vous le hack pour désactiver local-init? ce hack a un impact significatifstackalloc (c'est-à-dire qu'il n'efface pas l'espace alloué)
Marc Gravell
@ canton7 s'ils compilent vers le même IL, nous ne pouvons pas en déduire que c'est un bogue JIT ... si l'IL est le même, etc ... ressemble plus à un bogue du compilateur, si quelque chose, peut-être avec un compilateur plus ancien? György: pouvez-vous indiquer exactement comment vous compilez cela? quel SDK, par exemple? Je ne peux pas reprocher la poubelle
Marc Gravell
1
Il semble que stackalloc ne soit pas toujours nul, en fait: link
canton7

Réponses:

35

Ooh, c'est une trouvaille amusante; ce qui se passe ici, c'est que votre section locale est optimisée - il n'y a plus de section locale, ce qui signifie qu'il n'y en a pas .locals init, ce qui signifie que le stackalloccomportement est différent et n'efface pas l'espace;

private static unsafe uint Reinterpret1()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    return *(uint*)bytes;
}

private static unsafe uint Reinterpret2()
{
    byte* bytes = stackalloc byte[4];
    bytes[0] = 1;

    uint* asUint = (uint*)bytes;
    return *asUint;
}

devient:

.method private hidebysig static uint32 Reinterpret1() cil managed
{
    .maxstack 8
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: ldind.u4 
    L_0008: ret 
}

.method private hidebysig static uint32 Reinterpret2() cil managed
{
    .maxstack 3
    .locals init (
        [0] uint32* numPtr)
    L_0000: ldc.i4.4 
    L_0001: conv.u 
    L_0002: localloc 
    L_0004: dup 
    L_0005: ldc.i4.1 
    L_0006: stind.i1 
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldind.u4 
    L_000a: ret 
}

Je pense que je serais heureux de dire qu'il s'agit d'un bogue du compilateur, ou du moins: un effet secondaire et un comportement indésirables étant donné que des décisions antérieures ont été mises en place pour dire "émettre l'initialisation .locals" , en particulier pour essayer de rester stackallocsain d'esprit - mais si les gens du compilateur sont d'accord, c'est à eux de décider.

La solution de contournement est la suivante: traiter l' stackallocespace comme indéfini (ce qui, pour être juste, est ce que vous êtes censé faire); si vous pensez qu'il s'agit de zéros: mettez-le à zéro manuellement.

Marc Gravell
la source
2
Il semble qu'il y ait un ticket ouvert pour cela. Je vais ajouter un nouveau commentaire à cela.
György Kőszeg
Huh, tout mon travail et je n'ai pas remarqué que le premier manquait locals init. Joli.
canton7
1
@ canton7 si vous êtes comme moi, vous sautez automatiquement .maxstacket .locals, ce qui rend particulièrement facile de ne pas remarquer qu'il est / n'est pas là :)
Marc Gravell
1
The content of the newly allocated memory is undefined.selon MSDN. La spécification ne dit pas non plus que la mémoire doit être mise à zéro. Il semble donc que cela ne fonctionne que sur un ancien cadre par accident ou à la suite d'un comportement non contractuel.
Luaan