Переменные, заканчивающиеся на "1", удаляют "1" в ILSpy. Зачем?

Чтобы понять, как компилятор C# оптимизирует код, я создал простое тестовое приложение. С каждым изменением теста я компилировал приложение и затем открывал бинарный файл в ILSpy.

Я только заметил кое-что, что для меня странно. Очевидно, что это намеренно, однако я не могу придумать вескую причину, почему компилятор сделал бы это.

Рассмотрим следующий код:

static void Main(string[] args)
{
    int test_1 = 1;
    int test_2 = 0;
    int test_3 = 0;

    if (test_1 == 1) Console.Write(1);
    else if (test_2 == 1) Console.Write(1);
    else if (test_3 == 1) Console.Write(2);
    else Console.Write("x");
}

Бессмысленный код, но я написал это, чтобы увидеть, как ILSpy будет интерпретировать if заявления.

Однако, когда я компилировал / декомпилировал этот код, я заметил кое-что, что заставило меня почесать голову. Моя первая переменная test_1 был оптимизирован для test_! Есть ли веская причина, почему компилятор C# сделал бы это?

Для полной проверки это вывод Main() что я вижу в ILSpy.

private static void Main(string[] args)
{
    int test_ = 1; //Where did the "1" go at the end of the variable name???
    int test_2 = 0;
    int test_3 = 0;
    if (test_ == 1)
    {
        Console.Write(1);
    }
    else
    {
        if (test_2 == 1)
        {
            Console.Write(1);
        }
        else
        {
            if (test_3 == 1)
            {
                Console.Write(2);
            }
            else
            {
                Console.Write("x");
            }
        }
    }
}

ОБНОВИТЬ

По-видимому, после проверки IL, это проблема ILSpy, а не компилятора C#. Евгений Подскал дал хороший ответ на мои первоначальные комментарии и наблюдения. Тем не менее, мне интересно знать, является ли это скорее ошибкой в ​​ILSpy или это намеренная функциональность.

2 ответа

Решение

Ну, это ошибка. Не большая ошибка, довольно маловероятно, что кто-либо когда-либо подал отчет об ошибке для него. Обратите внимание, что ответ Евгения очень вводит в заблуждение. ildasm.exe достаточно умен, чтобы знать, как найти файл PDB для сборки и получить отладочную информацию для сборки. Который включает в себя имена локальных переменных.

Обычно это не роскошь, доступная дизассемблеру. Эти имена на самом деле не присутствуют в самой сборке, и они неизменно должны обходиться без PDB. То, что вы также можете увидеть в ildasm.exe, просто удалите файлы.pdb в каталогах obj\Release и bin\Release, и теперь это выглядит так:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       50 (0x32)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1,
           int32 V_2)
  IL_0000:  ldc.i4.1
  // etc...

Имена как V_0, V_1 и так далее, конечно, не велики, дизассемблер обычно предлагает что-то лучшее. Что-то вроде "Num".

Итак, довольно ясно, где находится ошибка в ILSpy, он тоже читает файл PDB, но шарит в найденном символе. Вы можете сообщить об ошибке поставщику, но вряд ли они отнесутся к ней как к высокоприоритетной.

Возможно, это какая-то проблема с декомпилятором. Потому что IL правильно на.NET 4.5 VS2013:

.entrypoint
  // Code size       79 (0x4f)
  .maxstack  2
  .locals init ([0] int32 test_1,
           [1] int32 test_2,
           [2] int32 test_3,
           [3] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0

редактировать: он использует данные из файла.pdb (см. этот ответ), чтобы получить правильные имена переменных. Без pdb он будет иметь переменные в форме V_0, V_1, V_2,

РЕДАКТИРОВАТЬ:

Имя переменной искажается в файле NameVariables.cs в методе:

public string GetAlternativeName(string oldVariableName)
{
    if (oldVariableName.Length == 1 && oldVariableName[0] >= 'i' && oldVariableName[0] <= maxLoopVariableName) {
        for (char c = 'i'; c <= maxLoopVariableName; c++) {
            if (!typeNames.ContainsKey(c.ToString())) {
                typeNames.Add(c.ToString(), 1);
                return c.ToString();
            }
        }
    }

    int number;
    string nameWithoutDigits = SplitName(oldVariableName, out number);

    if (!typeNames.ContainsKey(nameWithoutDigits)) {
        typeNames.Add(nameWithoutDigits, number - 1);
    }

    int count = ++typeNames[nameWithoutDigits];

    if (count != 1) {
        return nameWithoutDigits + count.ToString();
    } else {
        return nameWithoutDigits;
    }
}

NameVariables класс использует this.typeNames словарь для хранения имен переменных без конечного числа (такие переменные означают что-то особенное для ILSpy или, возможно, даже для IL, но я на самом деле в этом сомневаюсь), связанное со счетчиком их появления в методе декомпиляции.

Это означает, что все переменные (test_1, test_2, test_3) закончится в одном слоте ("test_") и для первого count var будет один, что приведет к выполнению:

else {
    return nameWithoutDigits;
}

где nameWithoutDigits является test_

РЕДАКТИРОВАТЬ

Во-первых, спасибо @HansPassant и его ответу за указание на ошибку в этом посте.

Итак, источник проблемы:

ILSpy столь же умный, как и ildasm, потому что он также использует данные.pdb (или как еще он получает test_1, test_2 имена вообще). Но его внутренняя работа оптимизирована для использования со сборками без какой-либо информации, связанной с отладкой, следовательно, его оптимизация связана с работой с V_0, V_1, V_2 Переменные работают несовместимо с множеством метаданных из файла.pdb.

Как я понимаю, виновником является оптимизация по удалению _0 от одиноких переменных.

Исправление этого, вероятно, потребует распространения факта использования данных.pdb в код генерации имен переменных.

Другие вопросы по тегам