29 votes

L'entrée et la sortie d'un bloc vérifié en C# ont-elles un coût ?

Considérez une boucle comme celle-ci :

for (int i = 0; i < end; ++i)
    // do something

Si je sais que i ne débordera pas, mais je veux une vérification contre le débordement, la troncature, etc., dans la partie "faire quelque chose". checked bloc à l'intérieur ou à l'extérieur de la boucle ?

for (int i = 0; i < end; ++i)
    checked {
         // do something
    }

o

checked {
    for (int i = 0; i < end; ++i)
         // do something
}

Plus généralement, le passage du mode coché au mode non coché a-t-il un coût ?

19voto

Justin Niessner Points 144953

Si vous voulez vraiment voir la différence, regardez quelques IL générées. Prenons un exemple très simple :

using System;

public class Program 
{
    public static void Main()
    {
        for(int i = 0; i < 10; i++)
        {
            var b = int.MaxValue + i;
        }
    }
}

Et on obtient :

.maxstack  2
.locals init (int32 V_0,
         int32 V_1,
         bool V_2)
IL_0000:  nop
IL_0001:  ldc.i4.0
IL_0002:  stloc.0
IL_0003:  br.s       IL_0013

IL_0005:  nop
IL_0006:  ldc.i4     0x7fffffff
IL_000b:  ldloc.0
IL_000c:  add
IL_000d:  stloc.1
IL_000e:  nop
IL_000f:  ldloc.0
IL_0010:  ldc.i4.1
IL_0011:  add
IL_0012:  stloc.0
IL_0013:  ldloc.0
IL_0014:  ldc.i4.s   10
IL_0016:  clt
IL_0018:  stloc.2
IL_0019:  ldloc.2
IL_001a:  brtrue.s   IL_0005

IL_001c:  ret

Maintenant, assurons-nous que nous sommes vérifiés :

public class Program 
{
    public static void Main()
    {
        for(int i = 0; i < 10; i++)
        {
            checked
            {
                var b = int.MaxValue + i;
            }
        }
    }
}

Et maintenant, nous obtenons le VA suivant :

.maxstack  2
.locals init (int32 V_0,
         int32 V_1,
         bool V_2)
IL_0000:  nop
IL_0001:  ldc.i4.0
IL_0002:  stloc.0
IL_0003:  br.s       IL_0015

IL_0005:  nop
IL_0006:  nop
IL_0007:  ldc.i4     0x7fffffff
IL_000c:  ldloc.0
IL_000d:  add.ovf
IL_000e:  stloc.1
IL_000f:  nop
IL_0010:  nop
IL_0011:  ldloc.0
IL_0012:  ldc.i4.1
IL_0013:  add
IL_0014:  stloc.0
IL_0015:  ldloc.0
IL_0016:  ldc.i4.s   10
IL_0018:  clt
IL_001a:  stloc.2
IL_001b:  ldloc.2
IL_001c:  brtrue.s   IL_0005

IL_001e:  ret

Comme vous pouvez le voir, la seule différence (à l'exception d'un peu plus de nop ) est que notre opération d'ajout émet add.ovf plutôt qu'un simple add . Les seuls frais généraux que vous accumulerez seront la différence entre ces opérations.

Maintenant, que se passe-t-il si on déplace le checked pour inclure l'ensemble du for boucle :

public class Program 
{
    public static void Main()
    {
        checked
        {
            for(int i = 0; i < 10; i++)
            {
                var b = int.MaxValue + i;
            }
        }
    }
}

Nous avons le nouveau IL :

.maxstack  2
.locals init (int32 V_0,
         int32 V_1,
         bool V_2)
IL_0000:  nop
IL_0001:  nop
IL_0002:  ldc.i4.0
IL_0003:  stloc.0
IL_0004:  br.s       IL_0014

IL_0006:  nop
IL_0007:  ldc.i4     0x7fffffff
IL_000c:  ldloc.0
IL_000d:  add.ovf
IL_000e:  stloc.1
IL_000f:  nop
IL_0010:  ldloc.0
IL_0011:  ldc.i4.1
IL_0012:  add.ovf
IL_0013:  stloc.0
IL_0014:  ldloc.0
IL_0015:  ldc.i4.s   10
IL_0017:  clt
IL_0019:  stloc.2
IL_001a:  ldloc.2
IL_001b:  brtrue.s   IL_0006

IL_001d:  nop
IL_001e:  ret

Vous pouvez voir que les deux add ont été converties en add.ovf plutôt que juste l'opération interne, donc vous obtenez deux fois la "surcharge". Dans tous les cas, je pense que la "surcharge" serait négligeable pour la plupart des cas d'utilisation.

9voto

checked y unchecked n'apparaissent pas au niveau des IL. Ils sont uniquement utilisés dans le code source C# pour indiquer au compilateur s'il doit choisir les instructions IL avec ou sans vérification lorsqu'il remplace la préférence par défaut de la configuration de construction (qui est définie par un drapeau de compilateur).

Bien sûr, il y aura généralement une différence de performance due au fait que des opcodes différents ont été émis pour les opérations arithmétiques (mais pas à cause de l'entrée ou de la sortie du bloc). On s'attend généralement à ce que l'arithmétique vérifiée ait une certaine surcharge par rapport à l'arithmétique non vérifiée correspondante.

En fait, considérez ce programme C# :

class Program
{
    static void Main(string[] args)
    {
        var a = 1;
        var b = 2;
        int u1, c1, u2, c2;

        Console.Write("unchecked add ");
        unchecked
        {
            u1 = a + b;
        }
        Console.WriteLine(u1);

        Console.Write("checked add ");
        checked
        {
            c1 = a + b;
        }
        Console.WriteLine(c1);

        Console.Write("unchecked call ");
        unchecked
        {
            u2 = Add(a, b);
        }
        Console.WriteLine(u2);

        Console.Write("checked call ");
        checked
        {
            c2 = Add(a, b);
        }
        Console.WriteLine(c2);
    }

    static int Add(int a, int b)
    {
        return a + b;
    }
}

Il s'agit de l'IL générée, avec les optimisations activées et l'arithmétique non vérifiée par défaut :

.class private auto ansi beforefieldinit Checked.Program
    extends [mscorlib]System.Object
{    
    .method private hidebysig static int32 Add (
            int32 a,
            int32 b
        ) cil managed 
    {
        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: add
        IL_0003: ret
    }

    .method private hidebysig static void Main (
            string[] args
        ) cil managed 
    {
        .entrypoint
        .locals init (
            [0] int32 b
        )

        IL_0000: ldc.i4.1
        IL_0001: ldc.i4.2
        IL_0002: stloc.0

        IL_0003: ldstr "unchecked add "
        IL_0008: call void [mscorlib]System.Console::Write(string)
        IL_000d: dup
        IL_000e: ldloc.0
        IL_000f: add
        IL_0010: call void [mscorlib]System.Console::WriteLine(int32)

        IL_0015: ldstr "checked add "
        IL_001a: call void [mscorlib]System.Console::Write(string)
        IL_001f: dup
        IL_0020: ldloc.0
        IL_0021: add.ovf
        IL_0022: call void [mscorlib]System.Console::WriteLine(int32)

        IL_0027: ldstr "unchecked call "
        IL_002c: call void [mscorlib]System.Console::Write(string)
        IL_0031: dup
        IL_0032: ldloc.0
        IL_0033: call int32 Checked.Program::Add(int32,  int32)
        IL_0038: call void [mscorlib]System.Console::WriteLine(int32)

        IL_003d: ldstr "checked call "
        IL_0042: call void [mscorlib]System.Console::Write(string)
        IL_0047: ldloc.0
        IL_0048: call int32 Checked.Program::Add(int32,  int32)
        IL_004d: call void [mscorlib]System.Console::WriteLine(int32)

        IL_0052: ret
    }
}

Comme vous pouvez le voir, le checked y unchecked sont simplement un concept de code source - il n'y a pas d'IL émis lors du passage d'un bloc à l'autre de ce qui était (dans la source) un bloc checked et un unchecked contexte. Ce qui change, ce sont les opcodes émis pour les opérations arithmétiques directes (dans ce cas, add y add.ovf ) qui étaient textuellement inclus dans ces blocs. La spécification couvre les opérations qui sont affectées :

Les opérations suivantes sont affectées par le contexte de contrôle de débordement établi par les opérateurs et les instructions contrôlés et non contrôlés :

  • Les opérateurs unaires prédéfinis ++ et -- (§7.6.9 et §7.7.5), lorsque l'opérande est de type intégral.
  • L'opérateur unaire prédéfini (§7.7.2), lorsque l'opérande est de type intégral.
  • Les opérateurs binaires prédéfinis +, -, * et / (§7.8), lorsque les deux opérandes sont de type intégral.
  • Conversions numériques explicites (§6.2.1) d'un type intégral à un autre type intégral, ou d'un flottant ou d'un double à un type intégral.

Et comme vous pouvez le voir, une méthode appelée à partir d'une checked o unchecked conservera son corps et ne recevra aucune information sur le contexte à partir duquel il a été appelé. Ceci est également précisé dans les spécifications :

Les opérateurs vérifiés et non vérifiés n'affectent le contexte de vérification du débordement que pour les opérations qui sont textuellement contenues dans les jetons "(" et ")". Les opérateurs n'ont aucun effet sur les membres de la fonction qui sont invoqués à la suite de l'évaluation de l'expression contenue.

Dans l'exemple

class Test
{
  static int Multiply(int x, int y) {
      return x * y;
  }
  static int F() {
      return checked(Multiply(1000000, 1000000));
  }
}

l'utilisation de checked dans F n'affecte pas l'évaluation de x * y dans Multiply, donc x * y est évalué dans le contexte de contrôle de débordement par défaut.

Comme indiqué, l'IL ci-dessus a été généré avec les optimisations du compilateur C# activées. Les mêmes conclusions peuvent être tirées de l'IL qui est émis sans ces optimisations.

6voto

Eldar Dordzhiev Points 2271

En plus des réponses ci-dessus, je veux clarifier comment fonctionne le contrôle. La seule méthode que je connaisse consiste à vérifier le OF y CF drapeaux. Le site CF est activé par les instructions arithmétiques non signées, tandis que l'indicateur OF est fixé par les instructions arithmétiques signées.

Ces drapeaux peuvent être lus avec la fonction seto\setc ou (la manière la plus utilisée) nous pouvons simplement utiliser les instructions jo\jc instruction de saut qui sautera à l'adresse désirée si le OF\CF est activé.

Mais, il y a un problème. jo\jc est un saut "conditionnel", qui est une véritable plaie pour le pipeline du CPU. J'ai donc pensé qu'il y avait peut-être un autre moyen de le faire, comme définir un registre spécial pour interrompre l'exécution lorsqu'un débordement est détecté, et j'ai décidé de découvrir comment le JIT de Microsoft fait cela.

Je suis sûr que la plupart d'entre vous ont entendu que Microsoft a ouvert la source du sous-ensemble de .NET qui est appelé .NET Core. Le code source de .NET Core inclut CoreCLR, alors j'ai creusé dedans. Le code de détection de débordement est généré dans le CodeGen::genCheckOverflow(GenTreePtr tree) (ligne 2484). On peut clairement voir que la méthode jo est utilisée pour le contrôle du dépassement de capacité signé et l'instruction jb (surprise !) pour le débordement non signé. Je n'ai pas programmé en assembleur depuis longtemps, mais il semble que jb y jc sont les mêmes instructions (elles vérifient toutes deux le drapeau de report uniquement). Je ne sais pas pourquoi les développeurs du JIT ont décidé d'utiliser l'instruction jb au lieu de jc parce que si j'étais un fabricant de CPU, je ferais un prédicteur de branche pour assumer jo\jc des sauts comme très peu probable de se produire.

En résumé, il n'y a pas d'instructions supplémentaires invoquées pour passer du mode coché au mode non coché, mais les opérations arithmétiques de l'option checked doit être sensiblement plus lent, tant que la vérification est effectuée après chaque instruction arithmétique. Cependant, je suis presque sûr que les CPU modernes peuvent bien gérer cela.

J'espère que cela vous aidera.

1voto

Kapoor Points 161

" Plus généralement, le passage du mode coché au mode non coché a-t-il un coût ? "

Non, pas dans votre exemple. La seule surcharge est le ++i .

Dans les deux cas, le compilateur C# générera add.ovf , sub.ovf , mul.ovf o conv.ovf .

Mais lorsque la boucle se trouve à l'intérieur d'un bloc contrôlé, il y aura un supplément de add.ovf para ++i

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X