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

Почему компилятор испускает инструкции ящика для сравнения экземпляров ссылочного типа?

Вот простой общий тип с уникальным общим параметром, ограниченным ссылочными типами:

class A<T> where T : class
{
    public bool F(T r1, T r2)
    {
        return r1 == r2;
    }
}

Сгенерированный IL файл csc.exe:

ldarg.1
box        !T
ldarg.2
box        !T
ceq

Итак, каждый параметр в коробке, прежде чем приступать к сравнению.

Но если ограничение указывает, что "T" никогда не должно быть типом значения, почему компилятор пытается вставить r1 и r2?

4b9b3361

Ответ 1

Требуется выполнить ограничения на проверяемость для сгенерированного ИЛ. Обратите внимание, что непроверяемый не обязательно означает неправильный. Он отлично работает без инструкции box, пока его контекст безопасности позволяет запускать непроверяемый код. Проверка является консервативной и основана на фиксированном наборе правил (например, достижимости). Чтобы упростить ситуацию, они решили не заботиться о наличии ограничений общего типа в алгоритме проверки.

Спецификация общей языковой инфраструктуры (ECMA-335)

Раздел 9.11: Ограничения на общие параметры

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

Удаление инструкций box приведет к непроверяемому коду:

.method public hidebysig instance bool 
       F(!T r1,
         !T r2) cil managed
{
   ldarg.1
   ldarg.2
   ceq
   ret
}


c:\Users\Mehrdad\Scratch>peverify sc.dll

Microsoft (R) .NET Framework PE Verifier.  Version  4.0.30319.1
Copyright (c) Microsoft Corporation.  All rights reserved.

[IL]: Error: [c:\Users\Mehrdad\Scratch\sc.dll : A`1[T]::F][offset 0x00000002][fo
und (unboxed) 'T'] Non-compatible types on the stack.
1 Error(s) Verifying sc.dll

ОБНОВЛЕНИЕ (ответьте на комментарий): Как я уже упоминал выше, проверяемость не эквивалентна правильности (здесь я говорю о "правильности" с точки зрения безопасности типа). Проверяемые программы являются строгим подмножеством правильных программ (т.е. Все проверяемые программы являются явно правильными, но есть правильные программы, которые не поддаются проверке). Таким образом, проверяемость является более сильным, чем правильность. Поскольку С# является языком Turing-complete, теорема Райса утверждает, что доказательство правильности программ неразрешимо в общем случае.

Вернемся к моей аналогичности достижимости, поскольку ее легче объяснить. Предположим, вы разрабатывали С#. Одна вещь, о которой подумали, - это когда выпустить предупреждение о недостижимом коде и полностью удалить этот фрагмент кода в оптимизаторе, но как вы собираетесь обнаруживать весь недостижимый код? Опять же, теорема Райса говорит, что вы не можете сделать это для всех программ. Например:

void Method() {
    while (true) {
    }
    DoSomething();  // unreachable code
}

Это то, о чем действительно предупреждает компилятор С#. Но он не предупреждает:

bool Condition() {
   return true;
}

void Method() {
   while (Condition()) {
   }
   DoSomething();  // no longer considered unreachable by the C# compiler
}

Человек может доказать, что поток управления никогда не достигает этой линии в последнем случае. Можно утверждать, что компилятор может статически доказать, что DoSomething также недостижим в этом случае, но это не так. Зачем? Дело в том, что вы не можете сделать это для всех программ, поэтому вы должны нарисовать линию в какой-то момент. На этом этапе вы должны определить свойство разрешимое и называть его "достижимостью". Например, для достижимости С# придерживается постоянных выражений и вообще не будет смотреть на содержимое функций. Простота анализа и согласованность дизайна являются важными целями при определении того, где рисовать линию.

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

Ответ 2

Ответ Мехрдада неплохой; Я просто хотел добавить пару очков:

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

Однако есть случаи, когда нужно держать верификатора счастливым, мы должны ввести инструкции по боксу, которые не оптимизированы. Например, если вы скажете:

class B<T> { public virtual void M<U>(U u) where U : T {...} }
class D : B<int> 
{ 
    public override void M<U>(U u)
    {

Компилятор С# знает, что в D.M U может быть только int. Тем не менее, чтобы быть поддающимся проверке, существуют ситуации, когда u должен быть помещен в бокс для объекта, а затем unboxed для int. Джиттер не всегда оптимизирует их; мы указали на команду дрожания, что это возможная оптимизация, но ситуация настолько неясна, что вряд ли это вызовет большую победу для многих реальных клиентов. Есть оптимизация с большими возможностями для того, чтобы они могли тратить свое время.