CLR оптимизация JIT нарушает причинную связь?

Я писал поучительный пример для коллеги, чтобы показать ему, почему тестирование плаваний для равенства часто является плохой идеей. Пример, с которым я пошел, добавлял.1 десять раз и выдерживал сравнение с 1,0 (тот, который мне показали в моем вводном числовом классе). Я был удивлен найти, что два результата были равны (код + вывод).

float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
    @float += 0.1f;
}
Console.WriteLine(@float == 1.0f);

Некоторое расследование показало, что на этот результат нельзя было положиться (во многом как равенство плавающее). Тот, который я нашел самым удивительным, был то, что, добавляя код после того, как другой код мог изменить результат вычисления (код + вывод). Обратите внимание, что этот пример имеет точно тот же код и IL с еще одной строкой добавленного C#.

float @float = 0.0f;
for(int @int = 0; @int < 10; @int += 1)
{
    @float += 0.1f;
}
Console.WriteLine(@float == 1.0f);
Console.WriteLine(@float.ToString("G9"));

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

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

Я искал немного, но не мог найти причину этого поведения. Может кто-либо подсказка меня в?

14
задан mskfisher 10 May 2012 в 14:11
поделиться

4 ответа

Это побочный эффект работы оптимизатора JIT. Он выполняет больше работы, если нужно сгенерировать меньше кода. Цикл в исходном фрагменте кода компилируется так:

                @float += 0.1f;
0000000f  fld         dword ptr ds:[0025156Ch]          ; push(intermediate), st0 = 0.1
00000015  faddp       st(1),st                          ; st0 = st0 + st1
            for (int @int = 0; @int < 10; @int += 1) {
00000017  inc         eax  
00000018  cmp         eax,0Ah 
0000001b  jl          0000000F 

Когда вы добавляете дополнительный оператор Console.WriteLine (), он компилирует его так:

                @float += 0.1f;
00000011  fld         dword ptr ds:[00961594h]          ; st0 = 0.1
00000017  fadd        dword ptr [ebp-8]                 ; st0 = st0 + @float
0000001a  fstp        dword ptr [ebp-8]                 ; @float = st0
            for (int @int = 0; @int < 10; @int += 1) {
0000001d  inc         eax  
0000001e  cmp         eax,0Ah 
00000021  jl          00000011 

Обратите внимание на разницу в адресе 15 и адресе 17 + 1a, первом цикле сохраняет промежуточный результат в FPU. Второй цикл сохраняет его обратно в локальную переменную @float. Пока он остается внутри FPU, результат вычисляется с полной точностью. Однако при его сохранении промежуточный результат обрезается до числа с плавающей запятой, теряя много бит точности в процессе.

Хотя это неприятно, я не верю, что это ошибка. Компилятор x64 JIT пока ведет себя иначе. Вы можете изложить свои аргументы на сайте connect.microsoft.com

18
ответ дан 1 December 2019 в 09:01
поделиться

Вы запускали это на Процессор Intel?

Согласно одной из теорий, JIT позволил @float полностью накапливаться в регистре с плавающей запятой, что обеспечивало бы полную точность 80 бит. Таким образом, расчет может быть достаточно точным.

Вторая версия кода не помещалась в регистры полностью, поэтому @float пришлось «пролить» в память, что привело к округлению 80-битного значения до одинарной точности, давая результаты, ожидаемые от арифметики одинарной точности.

Но это всего лишь очень случайное предположение. Нужно было бы проверить фактический машинный код, сгенерированный JIT-компилятором (отладка с открытым представлением дизассемблирования).

Редактировать:

Хм ... Я тестировал ваш код локально (Intel Core 2, Windows 7 x64, 64-битная среда CLR) и всегда получал "ожидаемую" ошибку округления. Как в выпуске, так и в отладочной конфигурации.

Ниже приведена дизассемблированная версия, отображаемая Visual Studio для первого фрагмента кода на моем компьютере:

xorps       xmm0,xmm0 
movss       dword ptr [rsp+20h],xmm0 
        for (int @int = 0; @int < 10; @int += 1)
mov         dword ptr [rsp+24h],0 
jmp         0000000000000061 
        {
            @float += 0.1f;
movss       xmm0,dword ptr [000000A0h] 
addss       xmm0,dword ptr [rsp+20h] 
movss       dword ptr [rsp+20h],xmm0 // <-- @float gets stored in memory
        for (int @int = 0; @int < 10; @int += 1)
mov         eax,dword ptr [rsp+24h] 
add         eax,1 
mov         dword ptr [rsp+24h],eax 
cmp         dword ptr [rsp+24h],0Ah 
jl          0000000000000042 
        }
        Console.WriteLine(@float == 1.0f);
etc.

Есть различий между JIT-компиляторами x64 и x86, но у меня нет доступа к 32-битная машина.

4
ответ дан 1 December 2019 в 09:01
поделиться

Моя теория, что без строки ToString компилятор может статически оптимизировать функцию до одного значения и что он каким-то образом компенсирует ошибку с плавающей запятой. Но когда добавляется строка ToString, оптимизатор должен обрабатывать float по-другому, потому что это требуется для вызова метода. Это просто предположение.

2
ответ дан 1 December 2019 в 09:01
поделиться

К вашему сведению, спецификация C # отмечает, что такое поведение является законным и распространенным. См. Эти вопросы для получения дополнительных сведений и аналогичных сценариев:

6
ответ дан 1 December 2019 в 09:01
поделиться
Другие вопросы по тегам:

Похожие вопросы: