Math.Max ​​против встроенного, если - в чем различия? - PullRequest
31 голосов
/ 30 марта 2011

Я работал над проектом сегодня и обнаружил, что использую Math.Max ​​в нескольких местах и ​​встроенные операторы if в других местах. Итак, мне было интересно, знает ли кто-нибудь, что "лучше" ... или, скорее, каковы реальные различия.

Например, в следующем, c1 = c2:

Random rand = new Random();
int a = rand.next(0,10000);
int b = rand.next(0,10000);

int c1 = Math.Max(a, b);
int c2 = a>b ? a : b;

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

Ответы [ 8 ]

31 голосов
/ 30 марта 2011

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

Math.Max(a,b) очень просто понять, независимо от предыдущих знаний кодирования.

a>b ? a : b потребует от пользователя некоторого знания троичного оператора, по крайней мере.

" Когда всомнение - иди на читабельность"

21 голосов
/ 02 июня 2012

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

Код выполняет миллиард циклов (да 1 миллиард).Вычитая издержки цикла, вы получите:

  • Math.Max ​​() потребовалось 0,0044 секунды для запуска 1 миллиард раз
  • Встроенное значение, если для запуска 1 миллиард раз потребовалось 0,0055 секунды

Я вычел накладные расходы, которые я рассчитал, выполнив пустой цикл 1 миллиард раз, накладные расходы составили 1,2 секунды.

Я запустил это на ноутбуке, 64-битная Windows 7,1,3 ГГц Intel Core i5 (U470).Код был скомпилирован в режиме выпуска и запущен без отладчика.

Вот код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace TestMathMax {
    class Program {
        static int Main(string[] args) {
            var num1 = 10;
            var num2 = 100;
            var maxValue = 0;
            var LoopCount = 1000000000;
            double controlTotalSeconds;
            { 
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (var i = 0; i < LoopCount; i++) {
                    // do nothing
                }
                stopwatch.Stop();
                controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
                Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
            }
            Console.WriteLine();
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++) {
                    maxValue = Math.Max(num1, num2);
                }
                stopwatch.Stop();
                Console.WriteLine("Math.Max() - " + stopwatch.Elapsed.TotalSeconds + " seconds");
                Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
            }
            Console.WriteLine();
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                for (int i = 0; i < LoopCount; i++) {
                    maxValue = num1 > num2 ? num1 : num2;
                }
                stopwatch.Stop();
                Console.WriteLine("Inline Max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
                Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
            }

            Console.ReadLine();

            return maxValue;
        }
    }
}

ОБНОВЛЕНО Результаты 2/7 / 2015

В Windows 8.1, Surface 3 Pro, i7 4650U 2,3 ГГц Работал как консольное приложение в режиме выпуска без отладчика.

  • Math.Max ​​() - 0,3194749 секунд
  • Макс. Встроенный: 0,3465041 секунд
14 голосов
/ 06 мая 2014

если заявление считается полезным

Краткое описание

оператор в форме if (a > max) max = a - это самый быстрый способ определения максимума набора чисел. Однако сама инфраструктура цикла занимает большую часть процессорного времени, поэтому такая оптимизация в конце концов сомнительна.

Подробнее

Ответ от luisperezphd интересен тем, что он предоставляет числа, однако я считаю, что метод некорректен: компилятор, скорее всего, переместит сравнение из цикла, поэтому ответ не измеряет то, что он хочет измерить. Это объясняет незначительную разницу во времени между контуром управления и контурами измерения.

Чтобы избежать этой оптимизации цикла, я добавил операцию, которая зависит от переменной цикла, в пустой цикл управления, а также во все измерительные циклы. Я моделирую общий случай нахождения максимума в списке чисел и использовал три набора данных:

  • лучший случай: первое число является максимальным, все числа после него меньше
  • худший случай: каждое число больше предыдущего, поэтому максимум меняет каждую итерацию
  • средний регистр: набор случайных чисел

См. Ниже код.

Результат был довольно неожиданным для меня. На моем ноутбуке Core i5 2520M я получил следующее за 1 миллиард итераций (пустой контроль занял около 2,6 с во всех случаях):

  • max = Math.Max(max, a): 2,0 с в лучшем случае / 1,3 с в худшем случае / 2,0 с в среднем
  • max = Math.Max(a, max): наилучший случай - 1,6 с / наихудший случай - 2,0 с / средний случай - 1,5 с *
  • max = max > a ? max : a: лучший случай 1,2 с / наихудший случай 1,2 с / средний случай 1,2 с
  • if (a > max) max = a: 0,2 с, наилучший случай / 0,9 с, наихудший случай / 0,3 с, средний случай

Таким образом, несмотря на длинные конвейеры ЦП и вытекающие из этого штрафы за ветвление, старый добрый оператор if является явным победителем для всех смоделированных наборов данных; в лучшем случае это в 10 раз быстрее, чем Math.Max, а в худшем - еще более чем на 30%.

Еще одним сюрпризом является то, что порядок аргументов Math.Max имеет значение. Предположительно это происходит из-за того, что логика предсказания ветвлений ЦП работает по-разному для этих двух случаев и неправильное предсказание ветвей более или менее в зависимости от порядка аргументов.

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

ОБНОВЛЕНО luisperezphd

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

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

По этой причине я сделал числа здесь относительно самого низкого времени вместо контрольного цикла. Секунды в результатах показывают, сколько времени прошло, чем самое быстрое время. Например, в результатах, приведенных ниже, самое быстрое время было для лучшего случая Math.Max ​​(a, max), поэтому каждый второй результат показывает, сколько времени они заняли.

Ниже приведены результаты, которые я получил:

  • max = Math.Max(max, a): лучший случай 0,012 с / наихудший случай 0,007 с / средний случай 0,028 с
  • max = Math.Max(a, max): 0,000 в лучшем случае / 0,021 в худшем случае / 0,019 с в среднем
  • max = max > a ? max : a: 0,022 с в лучшем случае / 0,02 с в худшем случае / 0,01 с в среднем
  • if (a > max) max = a: лучший случай 0,015 с / наихудший случай 0,024 с / средний случай 0,019 с

Во второй раз, когда я запустил его, я получил:

  • max = Math.Max(max, a): 0,024 с в лучшем случае / 0,010 с в худшем случае / 0,009 с в среднем
  • max = Math.Max(a, max): лучший случай 0,001 с / наихудший случай 0,000 с / средний случай 0,018 с
  • max = max > a ? max : a: 0,011 с в лучшем случае / 0,005 с в худшем случае / 0,018 с в среднем
  • if (a > max) max = a: лучший случай 0,000 с / наихудший случай 0,005 с / средний случай 0,039 с

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

Обратите внимание, что самое быстрое время, представленное в приведенных выше результатах 0,000, составляет около 8 секунд. Поэтому, если учесть, что самый длинный пробег составлял 8,039, разница во времени составляет около половины процента (0,5%) - слишком мало, чтобы иметь значение.

Компьютер

Код был запущен в Windows 8.1, i7 4810MQ 2,8 ГГц и скомпилирован в .NET 4.0.

Модификации кода

Я немного изменил ваш код для вывода результатов в формате, показанном выше. Я также добавил дополнительный код для ожидания через 1 секунду после начала учета любого дополнительного времени загрузки .NET может потребоваться при запуске сборки.

Также я выполнил все тесты дважды, чтобы учесть любые оптимизации процессора. Наконец, я изменил int для i на unit, чтобы я мог выполнить цикл 4 миллиарда раз вместо 1 миллиарда, чтобы получить больший промежуток времени.

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

Вы можете найти код по адресу: http://pastebin.com/84qi2cbD

код

using System;
using System.Diagnostics;

namespace ProfileMathMax
{
  class Program
  {
    static double controlTotalSeconds;
    const int InnerLoopCount = 100000;
    const int OuterLoopCount = 1000000000 / InnerLoopCount;
    static int[] values = new int[InnerLoopCount];
    static int total = 0;

    static void ProfileBase()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int maxValue;
        for (int j = 0; j < OuterLoopCount; j++)
        {
            maxValue = 0;
            for (int i = 0; i < InnerLoopCount; i++)
            {
                // baseline
                total += values[i];
            }
        }
        stopwatch.Stop();
        controlTotalSeconds = stopwatch.Elapsed.TotalSeconds;
        Console.WriteLine("Control - Empty Loop - " + controlTotalSeconds + " seconds");
    }

    static void ProfileMathMax()
    {
        int maxValue;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int j = 0; j < OuterLoopCount; j++)
        {
            maxValue = 0;
            for (int i = 0; i < InnerLoopCount; i++)
            {
                maxValue = Math.Max(values[i], maxValue);
                total += values[i];
            }
        }
        stopwatch.Stop();
        Console.WriteLine("Math.Max(a, max) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
        Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
    }

    static void ProfileMathMaxReverse()
    {
        int maxValue;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int j = 0; j < OuterLoopCount; j++)
        {
            maxValue = 0;
            for (int i = 0; i < InnerLoopCount; i++)
            {
                maxValue = Math.Max(maxValue, values[i]);
                total += values[i];
            }
        }
        stopwatch.Stop();
        Console.WriteLine("Math.Max(max, a) - " + stopwatch.Elapsed.TotalSeconds + " seconds");
        Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
    }

    static void ProfileInline()
    {
        int maxValue = 0;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int j = 0; j < OuterLoopCount; j++)
        {
            maxValue = 0;
            for (int i = 0; i < InnerLoopCount; i++)
            {
                maxValue = maxValue > values[i] ? values[i] : maxValue;
                total += values[i];
            }
        }
        stopwatch.Stop();
        Console.WriteLine("max = max > a ? a : max: " + stopwatch.Elapsed.TotalSeconds + " seconds");
        Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
    }

    static void ProfileIf()
    {
        int maxValue = 0;
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int j = 0; j < OuterLoopCount; j++)
        {
            maxValue = 0;
            for (int i = 0; i < InnerLoopCount; i++)
            {
                if (values[i] > maxValue)
                    maxValue = values[i];
                total += values[i];
            }
        }
        stopwatch.Stop();
        Console.WriteLine("if (a > max) max = a: " + stopwatch.Elapsed.TotalSeconds + " seconds");
        Console.WriteLine("Relative: " + (stopwatch.Elapsed.TotalSeconds - controlTotalSeconds) + " seconds");
    }

    static void Main(string[] args)
    {
        Random rnd = new Random();
        for (int i = 0; i < InnerLoopCount; i++)
        {
            //values[i] = i;  // worst case: every new number biggest than the previous
            //values[i] = i == 0 ? 1 : 0;  // best case: first number is the maximum
            values[i] = rnd.Next(int.MaxValue);  // average case: random numbers
        }

        ProfileBase();
        Console.WriteLine();
        ProfileMathMax();
        Console.WriteLine();
        ProfileMathMaxReverse();
        Console.WriteLine();
        ProfileInline();
        Console.WriteLine();
        ProfileIf();
        Console.ReadLine();
    }
  }
}
6 голосов
/ 30 марта 2011

Я бы сказал, что быстрее понять, что делает Math.Max, и это действительно должно быть единственным решающим фактором здесь.

Но, как поблажка, интересно учитывать, что Math.Max(a,b) оцениваетаргументы один раз, в то время как a > b ? a : b оценивает один из них дважды.Не проблема с локальными переменными, но для свойств с побочными эффектами, побочный эффект может произойти дважды.

6 голосов
/ 30 марта 2011

Если JITer решит встроить функцию Math.Max, исполняемый код будет идентичен оператору if. Если Math.Max ​​не встроен, он будет выполнен как вызов функции с вызовом и возвратными издержками, отсутствующими в операторе if. Таким образом, оператор if даст одинаковую производительность Math.Max ​​() во встроенном регистре или оператор if может быть на несколько тактов быстрее в не встроенном, но разница не будет заметна, если вы не используете десятки миллионов сравнений.

Поскольку разница в производительности между ними достаточно мала, чтобы быть незначительной в большинстве ситуаций, я бы предпочел Math.Max ​​(a, b), потому что его легче читать.

3 голосов
/ 19 мая 2018

Math.Max ​​(a, b)

равно НЕ эквивалентно - a > b ? a : b во всех случаях.

Math.Max возвращает большее значение двухаргументы, то есть:

if (a == b) return a; // or b, doesn't matter since they're identical
else if (a > b && b < a) return a;
else if (b > a && a < b) return b;
else return undefined;

Значение undefined отображается на double.NaN в случае двойной перегрузки, например, Math.Max.

a> b?a: b

оценивает a, если a больше, чем b, что не обязательно означает, что b меньше, чем a.

Простой пример, демонстрирующий, что они не эквивалентны:

var a = 0.0/0.0; // or double.NaN
var b = 1.0;
a > b ? a : b // evaluates to 1.0
Math.Max(a, b) // returns double.NaN
2 голосов
/ 19 марта 2014

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

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

Это нормальнопока не придет филиал (if, ?: и т. д.).Ветвь может нарушить последовательность и заставить ЦП уничтожить конвейер.Это стоит много тактов.

Теоретически, если компилятор достаточно умен, Math.Max может быть реализован с помощью встроенной команды ЦП, а ветвления можно избежать.

В этом случае Math.Max будет на самом деле быстрее, чем if - но это зависит от компилятора.

В случае более сложной работы Max - как работа с векторами, double []v; v.Max() компилятор может использоватьвысокооптимизированный библиотечный код, который может быть намного быстрее, чем обычный скомпилированный код.

Так что лучше использовать Math.Max, но также рекомендуется проверить вашу конкретную целевую систему и компилятор, если это достаточно важно..

0 голосов
/ 15 января 2019

взять операцию; N должно быть> = 0

Общие решения:

A) N = Math.Max(0, N)
B) if(N < 0){N = 0}

Сортировка по скорости:

Медленно: Math.Max ​​(A) <(B) оператор if-then: быстро (на 3% быстрее, чем решение 'A') </p>

Но мое решение на 4% быстрее, чем решение 'B':

N *= Math.Sign(1 + Math.Sign(N));
...