Подтвердить что ты не робот

Есть ли стоимость входа и выхода из контрольного блока С#?

Рассмотрим такой цикл:

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

Если я знаю, что я не буду переполняться, но я хочу проверить против переполнения, усечения и т.д. в части "сделать что-то", мне лучше с блоком checked внутри или вне цикла?

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

или

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

В более общем плане, существует ли стоимость переключения между проверенным и непроверенным режимами?

4b9b3361

Ответ 1

Если вы действительно хотите увидеть разницу, проверьте некоторые сгенерированные ИЛ. Возьмем очень простой пример:

using System;

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

И получим:

.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

Теперь убедитесь, что мы отмечены:

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

И теперь мы получаем следующий IL:

.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

Как вы можете видеть, единственная разница (за исключением некоторых дополнительных nop s) заключается в том, что наша операция добавления испускает add.ovf, а не просто add. Единственные накладные расходы, которые вы получите, - разница в этих операциях.

Теперь, что произойдет, если мы переместим блок checked, чтобы включить весь цикл for:

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

Получаем новый 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

Вы можете видеть, что обе операции add были преобразованы в add.ovf, а не только во внутреннюю операцию, поэтому вы получаете вдвое больше "накладных расходов". В любом случае, я предполагаю, что "накладные расходы" будут незначительными для большинства случаев использования.

Ответ 2

Блоки

checked и unchecked не отображаются на уровне IL. Они используются только в исходном коде С#, чтобы сообщить компилятору, следует ли выбирать контрольные или неконтролируемые IL-команды при переопределении предпочтения по умолчанию конфигурации сборки (которое устанавливается через флаг компилятора).

Конечно, обычно будет разница в производительности из-за того, что для арифметических операций (но не из-за ввода или выхода из блока) выбраны разные коды операций. Ожидается, что проверенная арифметика будет иметь некоторые накладные расходы по соответствующей непроверенной арифметике.

Собственно, рассмотрим эту программу С#:

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;
    }
}

Это сгенерированный ИЛ, с включенными оптимизациями и с непроверенной арифметикой по умолчанию:

.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
    }
}

Как вы можете видеть, блоки checked и unchecked - это просто концепция исходного кода - IL-излучение не испускается при переключении между тем, что было (в источнике) a checked и unchecked контекст. Какими изменениями являются коды операций, испускаемые для прямых арифметических операций (в данном случае add и add.ovf), которые были заключены в тексте в этих блоках. Спецификация описывает, какие операции затронуты:

Следующие операции зависят от контекста проверки переполнения, установленного проверенными и непроверенными операторами и операторами:

  • Предопределенные ++ и - унарные операторы (§7.6.9 и §7.7.5), когда операнд имеет интегральный тип.
  • Предопределенный - унарный оператор (§7.7.2), когда операнд имеет интегральный тип.
  • Предопределенные +, -, * и/двоичные операторы (§7.8), когда оба операнда являются интегральными типами.
  • Явные числовые преобразования (§6.2.1) от одного интегрального типа к другому интегральному типу или от float или double к интегральному типу.

И как вы можете видеть, метод, вызванный из блока checked или unchecked, сохранит свое тело и не получит никакой информации о том, из какого контекста он был вызван. Это также указано в спецификации:

Проверенные и непроверенные операторы влияют только на контекст проверки переполнения для тех операций, которые содержатся в текстовом виде в токенах ( "и" ). Операторы не влияют на члены функции, которые вызываются в результате оценки содержащегося выражения.

В примере

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

использование проверенного в F не влияет на оценку x * y в Multiply, поэтому x * y оценивается в контексте проверки переполнения по умолчанию.

Как отмечено, вышеуказанный IL был сгенерирован с оптимизацией компилятора С#. Те же выводы можно сделать из ИЛ, которые испускаются без этих оптимизаций.

Ответ 3

В дополнение к приведенным выше ответам я хочу уточнить, как выполняется проверка. Единственный метод, который я знаю, - это проверить флаги OF и CF. Флаг CF задается беззнаковыми арифметическими инструкциями, тогда как OF устанавливается с помощью арифметических инструкций.

Эти флаги могут быть прочитаны с помощью инструкций seto\setc или (наиболее часто используемый способ), мы можем просто использовать инструкцию перехода jo\jc, которая будет переходить на нужный адрес, если установлен флаг OF\CF.

Но есть проблема. jo\jc является "условным" прыжком, что является полной болью в *** для конвейера CPU. Поэтому я подумал, что может быть другой способ сделать это, например, установить специальный регистр для прерывания выполнения при обнаружении переполнения, поэтому я решил выяснить, как это делает Microsoft JIT.

Я уверен, что большинство из вас слышали, что Microsoft открыла подмножество .NET, которое называется .NET Core. Исходный код .NET Core включает CoreCLR, поэтому я в него вникнул. Код обнаружения переполнения генерируется в методе CodeGen::genCheckOverflow(GenTreePtr tree) (строка 2484). Можно четко видеть, что команда jo используется для проверки переполнения подписей и jb (сюрприз!) Для неподписанного переполнения. Я не запрограммирован в сборке в течение длительного времени, но это выглядит так: jb и jc являются теми же инструкциями (оба они проверяют только флаг переноса). Я не знаю, почему разработчики JIT решили использовать jb вместо jc, потому что если бы я был разработчиком процессора, я бы сделал предсказатель ветвления, чтобы предположить, что jo\jc скачки будут очень маловероятными.

Подводя итог, нет никаких дополнительных команд для переключения между проверенным и непроверенным режимами, но арифметические операции в блоке checked должны быть заметно медленнее, если проверка выполняется после каждой арифметической команды. Тем не менее, я уверен, что современные процессоры могут справиться с этим.

Надеюсь, это поможет.

Ответ 4

"В более общем плане, существует ли стоимость переключения между проверенным и непроверенным режимом?"

Нет, не в вашем примере. Единственные накладные расходы - ++i.

В обоих случаях компилятор С# будет генерировать add.ovf, sub.ovf, mul.ovf или conv.ovf.

Но когда цикл находится внутри проверочного блока, будет добавлен add.ovf для ++i