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

Несогласованная производительность умножения с поплавками

При тестировании производительности float в .NET я наткнулся на странный случай: для определенных значений умножение кажется медленнее, чем обычно. Вот тестовый пример:

using System;
using System.Diagnostics;

namespace NumericPerfTestCSharp {
    class Program {
        static void Main() {
            Benchmark(() => float32Multiply(0.1f), "\nfloat32Multiply(0.1f)");
            Benchmark(() => float32Multiply(0.9f), "\nfloat32Multiply(0.9f)");
            Benchmark(() => float32Multiply(0.99f), "\nfloat32Multiply(0.99f)");
            Benchmark(() => float32Multiply(0.999f), "\nfloat32Multiply(0.999f)");
            Benchmark(() => float32Multiply(1f), "\nfloat32Multiply(1f)");
        }

        static void float32Multiply(float param) {
            float n = 1000f;
            for (int i = 0; i < 1000000; ++i) {
                n = n * param;
            }
            // Write result to prevent the compiler from optimizing the entire method away
            Console.Write(n);
        }

        static void Benchmark(Action func, string message) {
            // warm-up call
            func();

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < 5; ++i) {
                func();
            }
            Console.WriteLine(message + " : {0} ms", sw.ElapsedMilliseconds);
        }
    }
}

Результаты:

float32Multiply(0.1f) : 7 ms
float32Multiply(0.9f) : 946 ms
float32Multiply(0.99f) : 8 ms
float32Multiply(0.999f) : 7 ms
float32Multiply(1f) : 7 ms

Почему результаты имеют разные значения для param = 0.9f?

Параметры тестирования:.NET 4.5, сборка выпусков, оптимизация кода ВКЛ, x86, без отладчика.

4b9b3361

Ответ 1

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

Освещается понимание того, почему происходит переход на .5:

Предположим, что вы умножаетесь на p. В конце концов, значение становится настолько малым, что результатом является некоторое субнормальное значение (ниже 2 -126 в 32-битной двоичной плавающей точке IEEE). Тогда умножение становится медленным. По мере продолжения умножения значение продолжает уменьшаться и достигает 2 -149 что является наименьшим положительным числом, которое может быть представлено. Теперь, когда вы умножаетесь на p, точный результат, конечно, 2 -149 p, который находится между 0 и 2 - 149 которые являются двумя ближайшими представляемыми значениями. Машина должна округлить результат и вернуть одно из этих значений.

Какой? Если p меньше ½, то 2 -149 p ближе к 0, чем к 2 -149 поэтому машина возвращает 0. Затем вы больше не работаете с субнормальными значениями, а умножение происходит быстро. Если p больше ½, то 2 -149 p ближе к 2 -149 чем к 0, поэтому машина возвращает 2 -149 и вы продолжаете работать с субнормальными значениями, а умножение остается медленным. Если p ровно ½, правила округления говорят использовать значение, которое имеет нулевое значение в младшем бите его значащей части (фракции), которая равна нулю (2 -149 имеет 1 в своем младшем разряде).

Вы сообщаете, что .99f появляется быстро. Это должно закончиться медленным поведением. Возможно, код, который вы опубликовали, не совсем тот код, для которого вы измеряли быструю производительность с помощью .99f? Возможно, изменилось начальное значение или количество итераций?

Есть способы обойти эту проблему. Во-первых, аппаратное обеспечение имеет настройки режима, которые определяют изменение любых субнормальных значений, используемых или полученных до нуля, называемых режимами "denormals as zero" или "flush to zero". Я не использую .NET и не могу сообщить вам, как установить эти режимы в .NET.

Другой подход заключается в том, чтобы каждый раз добавлять крошечное значение, например

n = (n+e) * param;

где e составляет не менее 2 -126/param. Обратите внимание, что 2 -126/param должен быть рассчитан округлен вверх, если вы не можете гарантировать, что n достаточно велико, чтобы (n+e) * param не выдавал субнормальное значение, Это также предполагает, что n не является отрицательным. Эффект этого заключается в том, чтобы удостовериться, что вычисленное значение всегда достаточно велико, чтобы быть в нормальном диапазоне, никогда не было ненормальным.

Добавление e таким образом, конечно, изменяет результаты. Однако, если вы, например, обрабатываете звук с некоторым эффектом эха (или другим фильтром), то значение e слишком мало, чтобы вызвать любые эффекты, наблюдаемые людьми, слушающими звук. Это, вероятно, слишком мало, чтобы вызывать какие-либо изменения в аппаратном поведении при создании звука.

Ответ 2

Я подозреваю, что это имеет какое-то отношение к денормальным значениям (значения fp меньше ~ 1e-38) и стоимости, связанные с их обработкой.

Если вы проверяете денормальные значения и удаляете их, работоспособность восстанавливается.

    static void float32Multiply(float param) {
        float n = 1000f;
        int zeroCount=0;
        for (int i = 0; i < 1000000; ++i) {
            n = n * param;
            if(n<1e-38)n=0;
        }
        // Write result to prevent the compiler from optimizing the entire method away
        Console.Write(n);
    }