быстрая функция знака c

#c #benchmarking #timing

Вопрос:

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

Моя проверка знака-довольно элементарный расчет fabs() , поэтому я решил, что должны быть другие способы сделать это, которые, вероятно, быстрее, так как «деление происходит медленно». Я наткнулся на функцию шаблона и copysign() создал простую программу для сравнения скорости. Я протестировал три возможных решения с помощью приведенного ниже кода.

 // C   program to find out execution time of  of functions 
#include <chrono> 
#include <iostream> 
#include <math.h>

using namespace std; 
using namespace std::chrono; 

template<typename Clock>

void printResult(const std::string name, std::chrono::time_point<Clock> start, std::chrono::time_point<Clock> stop, const int iterations)
{
    // Get duration. 
    std::chrono::duration my_duration = duration_cast<nanoseconds>(stop - start); 
    my_duration /= iterations;

    cout << "Time taken by "<< name <<" function: " << my_duration.count() << " ns avg. for " << iterations << " iterations." << endl << endl; 
}


template <typename T> int sgn(T val) 
{
    return (T(0) < val) - (val < T(0));
}


int main() {

    // ***************************************************************** //
    int numiters = 100000000;
    double vel = -0.6574;
    double result = 0;
    
    // Get starting timepoint 
    auto start_1 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x  ) 
    {

        result = (vel/fabs(vel)) * 12.1;

    }

    // Get ending timepoint 
    auto stop_1 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("fabs", start_1, stop_1, numiters);

    // Get starting timepoint 
    result = 0;
    auto start_2 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x  ) 
    {

        result = sgn(vel) * 12.1;

    }

    // Get ending timepoint 
    auto stop_2 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("sgn", start_2, stop_2, numiters);


    // Get starting timepoint 
    result = 0;
    auto start_10 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x  ) 
    {

        result = copysign(12.1, vel);

    }

    // Get ending timepoint 
    auto stop_10 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("copysign", start_10, stop_10, numiters);

    cout << endl;


}
 

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

Правильно ли я рассчитал время? И есть ли лучший способ сделать то, что я делаю, чем три примера, которые я проверил?

Обновить

Я внедрил тесты на quick-bench.com где можно указать настройку компилятора, и все 3 результата кажутся там почти идентичными. Я думаю, что, возможно, я что-то не так понял: https://quick-bench.com/q/PJiAmoC2NQIJyuvbdz5ZHUALu2M

Комментарии:

1. Существует знак std:: . В IEEE 754 (который обычно используется для float и double ) знаком с плавающей запятой является дополнительный бит. Таким образом, я ожидаю, что это будет очень быстро.

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

3. Согласно Проводнику компилятора , на самом деле это похоже на простую проверку битов (если я правильно интерпретировал ASM).

4. Какие флаги компилятора вы используете? Есть ли шанс, что вы могли бы переписать это как микробенчмарку, используя quick-bench.com

5. Написание тестов производительности не так просто. Легко совершить невидимую ошибку, которая приведет к неправильным результатам. Флаги сборки важны, также код должен быть написан тщательно, чтобы компилятор не удалял проверенный код (правило ASIF дает такую возможность). Обратите внимание на комментарий выше и ссылку, предоставленную на онлайн-инструмент.

Ответ №1:

Ваши тесты недействительны, потому что вы блокируете ввод-вывод внутри времени.

Однако мы можем использовать быстрый стенд для анализа: https://quick-bench.com/q/gt2KzKOFP4iV3ajmqANL_MhnMZk. Это показывает, что все тайминги практически идентичны. Как насчет ассемблерного кода, сгенерированного компилятором?

 double result = (vel/fabs(vel)) * 12.1;
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx


double result = sgn(vel) * 12.1;
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx


double result = copysign(12.1, vel);
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx
 

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

Комментарии:

1.@MooningDuck вы неправильно прочитали сборку. Это легче прочитать на godbolt. Смотрите мой ответ. В основном цикл содержит только две инструкции: mov add .

2. @MarekR: Я в принципе вообще не могу читать сборку, но дело в том, что они идентичны 😛

Ответ №2:

Как я уже говорил, ваш тест ничего не измеряет!

От вашего quick-bench.com ссылка нажмите на значок godbolt и посмотрите эту разборку.

Примечание. Все ваши версии преобразованы в этот код сборки:

         movabs  rax, -4600370724363619533 # compile time evaluated result move outside measurement loop
.LBB0_3:                                  # =>This Inner Loop Header: Depth=1
        mov     qword ptr [rsp   8], rax
        add     rbx, -1                   # measurement loop counter
        jne     .LBB0_3
 

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

Таким образом, вам нужно подать, чтобы проверить некоторое значение, которое компилятор не сможет определить во время компиляции.

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

Комментарии:

1. Большое спасибо, эти результаты, по крайней мере, имеют смысл — все они занимают больше времени, чем Noop значение, и я ожидал fabs бы, что это будет медленнее, но я не знал, как проверить эту теорию. Медленно учусь!

2. Да, std::copysign() очевидно, что наиболее эффективно просто andps изолировать знаковый бит и orps применить его. (Поскольку 12.1 это известная положительная константа, andps в ней нет необходимости.) sgn Тесты не идеальны, потому что компилятор оптимизирует i%4 индексирование массива в / — результат, основанный только на индексе, хотя на самом деле не использует двойные значения.