Как повторить сегмент кода без использования функции или класса для высокопроизводительного цикла в C ++ - PullRequest
2 голосов
/ 27 марта 2019

Моя программа на C ++ 11 выполняет онлайн-обработку сериализованных данных, и цикл должен выполнять миллионы позиций в памяти.Эффективность вычислений является обязательной, и меня беспокоит то, что вызов функции или класса в таком цикле создаст ненужные операции, которые будут влиять на эффективность, например, передачу нескольких значений указателя, необходимых для операции, между различными областями переменных.

Для примера рассмотрим следующий фиктивный пример, где «что-то» - это повторяющаяся операция.Обратите внимание, что в коде «что-то» используются переменные внутри области видимости цикла.

do {
    something(&span,&foo);
    spam++
    foo++
    if ( spam == spam_spam ) {
      something(&span,&foo);
      other_things(&span,&foo);
      something(&span,&foo);
    }
    else {
      something(&span,&foo);
      still_other_things(&span,&foo);
      something(&span,&foo);
    }
}
while (foo<bar);

Есть ли способ повторить блок кода и избежать перемещения и копирования переменных с помощью ненужных операций?Действительно ли использование функций и классов в таких циклах подразумевает дополнительные операции и как их можно избежать?


Обновление

Как и предполагалось, я провел несколькотесты с кодом, представленным ниже.Я протестировал несколько вариантов того, как вызывать простое приращение в 100 миллионов раз.Я использую GCC поверх RHEL 7 Server 7.6 на виртуальной машине x86_64 под Hyper-V.

Изначально компиляция с помощью "g ++ -std = c ++ 17 -o test.o test.cpp"

  • Простое вычисление цикла (базовое значение): 211,046 мс

  • Встроенная функция: 468,768 мс

  • Лямбда-функция: 253,466мс

  • Определить макрос: 211,995 мс

  • Передаваемые значения функции: 466,986мс

  • Указатели передачи функций: 344,646мс

  • Функция с пустотой: 190,557мс

  • Метод объекта, работающий с элементами: 231,458мс

  • Методы объекта, передающие значения: 227,615 мс

Из этих результатов я понял, что компилятор не принимает встроенное предложение, даже после попытки выпуклости сделать это какпредложено в g ++ не встроенные функции

Позже, как предложено в ответе Мэта на тот же пост, я включил opti компилятораМиграции с использованием "g ++ -std = c ++ 17 -O2 -o test.o test.cpp" и получили следующий результат при том же количестве итераций по сравнению с тестом без оптимизации.

  • Простой цикл вычислений (базовый уровень): 62,9254 мс

  • Встроенная функция: 65,0564 мс

  • Лямбда-функция: 32,8637 мс

  • Определить макрос: 63,0299 мс

  • Значения передачи функций: 64,2876 мс

  • Указатели передачи функций: 63,3416 мс

  • Функция с недействительными значениями: 32,1073 мс

  • Метод объекта, работающий с элементами: 63,3847 мс

  • Объектметод, передающий значения: 62,5151мс

Заключение к этому пункту:

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

  • «определение макросов» и «лямбда-функции» являются лучшей альтернативой встроенному.У каждого есть свои преимущества и возможности, #define является более гибким.

  • Использование членов объекта и методов обеспечивает хороший баланс для решения проблемы в любой ситуации, сохраняя код в форме, котораяпроще в обслуживании и оптимизации.

  • настройка компилятора того стоит;

Следует коду, использованному для тестирования:

// Libraries
    #include <iostream>
    #include <cmath>
    #include <chrono>

// Namespaces
    using namespace std;
    using namespace std::chrono;

// constants that control program behaviour
    const long END_RESULT = 100000000;
    const double AVERAGING_LENGTH = 40.0;
    const int NUMBER_OF_ALGORITHM = 9;
    const long INITIAL_VALUE = 0;
    const long INCREMENT = 1;

// Global variables used for test with void function and to general control of the program;
    long global_variable;
    long global_increment;

// Function that returns the execution time for a simple loop
int64_t simple_loop_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Perform the computation for baseline
        do {
            local_variable += local_increment;
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return(duration_cast<microseconds>( timer_stop - timer_start ).count());
}

// Functions that computes the execution time when using inline code within the loop
inline long increment_variable() __attribute__((always_inline));
inline long increment_variable(long local_variable, long local_increment) {
    return local_variable += local_increment;
}

int64_t inline_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Perform the computation for baseline
        do {
            local_variable = increment_variable(local_variable,local_increment);
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}

// Functions that computes the execution time when using lambda code within the loop
int64_t labda_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // define lambda function
        auto lambda_increment = [&] {
            local_variable += local_increment;
        };

    // Perform the computation for baseline
        do {
            lambda_increment();
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}

// define lambda function
    #define define_increment() local_variable += local_increment;

// Functions that computes the execution time when using lambda code within the loop
int64_t define_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Perform the computation for baseline
        do {
            define_increment();
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}
// Functions that compute the execution time when calling a function within the loop passing variable values
long increment_with_values_function(long local_variable, long local_increment) {
    return local_variable += local_increment;
}

int64_t function_values_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Perform the computation for baseline
        do {
            local_variable = increment_with_values_function(local_variable,local_increment);
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}
// Functions that compute the execution time when calling a function within the loop passing variable pointers
long increment_with_pointers_function(long *local_variable, long *local_increment) {
    return *local_variable += *local_increment;
}

int64_t function_pointers_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Perform the computation for baseline
        do {
            local_variable = increment_with_pointers_function(&local_variable,&local_increment);
        } while ( local_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}
// Functions that compute the execution time when calling a function within the loop without passing variables 
void increment_with_void_function(void) {
    global_variable += global_increment;
}

int64_t function_void_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // set global variables
        global_variable = local_variable;
        global_increment = local_increment;

    // Perform the computation for baseline
        do {
            increment_with_void_function();
        } while ( global_variable != END_RESULT);

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}
// Object and Function that compute the duration when using a method of the object where data is stored without passing variables
struct object {
    long object_variable = 0;
    long object_increment = 1;

    object(long local_variable, long local_increment) {
        object_variable = local_variable;
        object_increment = local_increment;
    }

    void increment_object(void){
        object_variable+=object_increment;
    }

    void increment_object_with_value(long local_increment){
        object_variable+=local_increment;
    }
};

int64_t object_members_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Create object
        object object_instance = {local_variable,local_increment};

    // Perform the computation for baseline
        do {
            object_instance.increment_object();
        } while ( object_instance.object_variable != END_RESULT);

    // Get the results out of the object
        local_variable = object_instance.object_variable;

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}

// Function that compute the duration when using a method of the object where data is stored passing variables
int64_t object_values_computation(long local_variable, long local_increment) {
    // Starts the clock to measure the execution time for the baseline
        high_resolution_clock::time_point timer_start = high_resolution_clock::now();

    // Create object
        object object_instance = {local_variable,local_increment};

    // Perform the computation for baseline
        do {
            object_instance.increment_object_with_value(local_increment);
        } while ( object_instance.object_variable != END_RESULT);

    // Get the results out of the object
        local_variable = object_instance.object_variable;

    // Stop the clock to measure performance of the silly version
        high_resolution_clock::time_point timer_stop = high_resolution_clock::now();

        return duration_cast<microseconds>( timer_stop - timer_start ).count();
}

int main() {

    // Create array to store execution time results for all tests
        pair<string,int64_t> duration_sum[NUMBER_OF_ALGORITHM]={
            make_pair("Simple loop computation (baseline): ",0.0),
            make_pair("Inline Function: ",0.0),
            make_pair("Lambda Function: ",0.0),
            make_pair("Define Macro: ",0.0)
            make_pair("Function passing values: ",0.0),
            make_pair("Function passing pointers: ",0.0),
            make_pair("Function with void: ",0.0),
            make_pair("Object method operating with members: ",0.0),
            make_pair("Object method passing values: ",0.0),
        };

    // loop to compute average of several execution times
        for ( int i = 0; i < AVERAGING_LENGTH; i++) {
            // Compute the execution time for a simple loop as the baseline
                duration_sum[0].second = duration_sum[0].second + simple_loop_computation(INITIAL_VALUE, INCREMENT);

            // Compute the execution time when using inline code within the loop (expected same as baseline)
                duration_sum[1].second = duration_sum[1].second + inline_computation(INITIAL_VALUE, INCREMENT);

            // Compute the execution time when using lambda code within the loop (expected same as baseline)
                duration_sum[2].second = duration_sum[2].second + labda_computation(INITIAL_VALUE, INCREMENT);

            // Compute the duration when using a define macro
                duration_sum[3].second = duration_sum[3].second + define_computation(INITIAL_VALUE, INCREMENT);

            // Compute the execution time when calling a function within the loop passing variables values
                duration_sum[4].second = duration_sum[4].second + function_values_computation(INITIAL_VALUE, INCREMENT);

            // Compute the execution time when calling a function within the loop passing variables pointers
                duration_sum[5].second = duration_sum[5].second + function_pointers_computation(INITIAL_VALUE, INCREMENT);

            // Compute the execution time when calling a function within the loop without passing variables
                duration_sum[6].second = duration_sum[6].second + function_void_computation(INITIAL_VALUE, INCREMENT);

            // Compute the duration when using a method of the object where data is stored without passing variables
                duration_sum[7].second = duration_sum[7].second + object_members_computation(INITIAL_VALUE, INCREMENT);

            // Compute the duration when using a method of the object where data is stored passing variables
                duration_sum[8].second = duration_sum[8].second + object_values_computation(INITIAL_VALUE, INCREMENT);
        }


        double average_baseline_duration = 0.0;

    // Print out results
        for ( int i = 0; i < NUMBER_OF_ALGORITHM; i++) {
        // compute averave from sum
            average_baseline_duration = ((double)duration_sum[i].second/AVERAGING_LENGTH)/1000.0;

        // Print the result
            cout << duration_sum[i].first << average_baseline_duration << "ms \n";
        }

    return 0;
}

Ответы [ 2 ]

4 голосов
/ 27 марта 2019

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

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

1 голос
/ 28 марта 2019

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

Так что, если вы хотите лучшее решение без ограничений (для более сложного кода), которое работает всегда, я обычно использую #define для этого:

// definition
#define code_block1(operands) { code ... }
#define code_block2(operands) { code ... \
 code ... \
 code ... \
 code ... \
 code ... }

// usage:

code ...
code_block1(); // this is macro so the last ; is not needed but I like it there ...
code_block2();

code ...
code_block2();

code ...
code_block1();

code ...
code_block2();
code_block1();
...

// undefinition so tokens do not fight with latter on code
#undef code_block1
#undef code_block2

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

for (i=0;i<100;i++) code_block1();

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

Для короткого кода вы можете записать материал в одну строку, но если код длинный, вы можете разделить определение на несколько строк, используя \. Остерегайтесь не использовать комментарии // внутри строк определения, так как это закомментирует все, даже код после используемого макроса в вашем коде ... так что если у вас должны быть комментарии, используйте /* ... */ вместо.

Часть (operands) является необязательной, для операндов она не будет

#define code_block1 { code ... }

Часть #undef является необязательной, зависит от того, хотите ли вы, чтобы такие макросы использовались во всем коде или просто локально в какой-либо функции, классе, файле ... Также, как вы можете видеть, в макросе используется только имя токена. вообще никаких операндов.

Я использую это много, например, см .:

и найдите loop_beg и loop_end ... Это макрос цикла с использованием:

loop_beg custom_code; loop_end

, поэтому у него нет {}, поскольку { находится в loop_beg, а } находится в loop_end.

...