Почему время выполнения одного и того же определения функции в классе медленнее более чем в 10 раз?

Не уверен, какой вид оптимизации выполняет compiler, но почему внутри класса одно и то же определение функции работает медленнее, чем то же, что и глобальный метод?

#include <iostream>
#include <chrono>

#define MAX_BUFFER 256
const int whileLoops = 1024 * 1024 * 10;

void TracedFunction(int blockSize) {
    std::chrono::high_resolution_clock::time_point pStart;
    std::chrono::high_resolution_clock::time_point pEnd;

    double A[MAX_BUFFER];
    double B[MAX_BUFFER];
    double C[MAX_BUFFER];

    // fill A/B
    for (int sampleIndex = 0; sampleIndex < MAX_BUFFER; sampleIndex++) {
        A[sampleIndex] = sampleIndex;
        B[sampleIndex] = sampleIndex + 1000.0;
    }

    // same traced function
    pStart = std::chrono::high_resolution_clock::now();

    int whileCounter = 0;
    while (whileCounter < whileLoops) {
        for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
            double value = A[sampleIndex] + B[sampleIndex];

            C[sampleIndex] = value;
        }

        whileCounter++;
    }

    pEnd = std::chrono::high_resolution_clock::now();
    std::cout << "execution time: " << std::chrono::duration_cast<std::chrono::milliseconds>(pEnd - pStart).count() << " ms" << " | fake result: " << A[19] << " " << B[90] << " " << C[129] << std::endl;
}

class OptimizeProcess
{
public:
    std::chrono::high_resolution_clock::time_point pStart;
    std::chrono::high_resolution_clock::time_point pEnd;

    double A[MAX_BUFFER];
    double B[MAX_BUFFER];
    double C[MAX_BUFFER];

    OptimizeProcess() {
        // fill A/B
        for (int sampleIndex = 0; sampleIndex < MAX_BUFFER; sampleIndex++) {
            A[sampleIndex] = sampleIndex;
            B[sampleIndex] = sampleIndex + 1000.0;
        }
    }

    void TracedFunction(int blockSize) {
        // same traced function
        pStart = std::chrono::high_resolution_clock::now();

        int whileCounter = 0;
        while (whileCounter < whileLoops) {
            for (int sampleIndex = 0; sampleIndex < blockSize; sampleIndex++) {
                double value = A[sampleIndex] + B[sampleIndex];

                C[sampleIndex] = value;
            }

            whileCounter++;
        }

        pEnd = std::chrono::high_resolution_clock::now();
        std::cout << "execution time: " << std::chrono::duration_cast<std::chrono::milliseconds>(pEnd - pStart).count() << " ms" << " | fake result: " << A[19] << " " << B[90] << " " << C[129] << std::endl;
    }
};

int main() {
    int blockSize = MAX_BUFFER;

    // outside class
    TracedFunction(blockSize);

    // within class
    OptimizeProcess p1;
    p1.TracedFunction(blockSize);

    std::cout << std::endl;
    system("pause");

    return 0;
}

Пробовал с MSVC, /Oi /Ot.

~ 80 мс против 1200 мс. Есть ли разворачивание цикла с использованием blockSize в качестве константы на compile-time?

Не уверен, так как я пытался установить blockSize случайным образом с помощью:

std::mt19937_64 gen{ std::random_device()() };
std::uniform_real_distribution<double> dis{ 0.0, 1.0 };

int blockSize = dis(gen) * 255 + 1;

Те же результаты ...

Вы компилировали с включенной оптимизацией? Потому что здесь я получаю почти одинаковую цифру для обоих исполнений: ideone.com/uZK787

Christophe 27.10.2018 11:37

@Christophe: да, как я уже сказал, /Oi /Ot

markzzz 27.10.2018 11:40

А / O2, все тот же?

Christophe 27.10.2018 11:43

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

muradm 27.10.2018 11:43

Вывод на моем компьютере: execution time: 9647 ms | fake result: 19 1090 1258 execution time: 9565 ms | fake result: 19 1090 1258. Я использую компилятор gcc, Ubuntu 16.04.

CoralK 27.10.2018 11:47

Я получаю аналогичные тайминги в VC++ 2017 с полной оптимизацией: execution time: 68 ms | fake result: 2.22523e-306 2.20691e-312 1.13591e-305 execution time: 1008 ms | fake result: 2.22523e-306 4.51917e-309 1.13592e-305

zett42 27.10.2018 11:47

Звонок system(), правда? Почему? Кстати, я вижу только 1.4 ускорение, но все же .. С O2.

gsamaras 27.10.2018 11:47

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

Pezo 27.10.2018 11:47

С массивами с нулевой инициализацией: 74 мс против 1024 мс. Бьюсь об заклад, компилятор оптимизирует большинство циклов, поскольку наблюдаемый эффект не зависит от всего результата.

zett42 27.10.2018 11:50

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

Pezo 27.10.2018 11:51

@Pezo, да, верно пропустил / предположил OptimizeProcess p1.

muradm 27.10.2018 11:53

@Pezo: если я инициализирую оба массива в обеих функциях, ничего не изменится (все равно 80 против 1200 мс ~).

markzzz 27.10.2018 11:55

Странно, я сейчас на мобильном телефоне, поэтому не могу проверить, извините.

Pezo 27.10.2018 11:56

Компиляция одного и того же слегка измененный код (удаление iostream / chrono и создание массивов глобальными, чтобы компилятор не оптимизировал их) на Godbolt показывает, что для обеих версий генерируется один и тот же код. Таким образом, дальнейшее обсуждение бессмысленно.

Daniel Kamil Kozar 27.10.2018 11:56

О, кстати, вы смотрели сгенерированную сборку? Может быть, опубликуйте ссылку Godbolt.

Pezo 27.10.2018 11:56

@muradm: я думаю, что это должно быть как Pezo, но если я сделаю так, как вы предлагаете (т.е. переместите массив / init в основной и передайте их как указатель, оба будут около 80 мс): O Очень странно

markzzz 27.10.2018 11:57

@DanielKamilKozar: как вы "инициализируете" массивы? В основном так? Можете показать весь пример? Не уверен, почему, если я инициализирую функцию, она должна ее удалить: O

markzzz 27.10.2018 12:02

@DanielKamilKozar: смотрит мой обновленный ответ с массивами inits: на самом деле он их не убирает (поскольку std :: cout выводит правильные значения). Так что все еще не уверен, что происходит ...

markzzz 27.10.2018 12:03

Здесь вы можете видеть, что gcc почти генерирует один и тот же код (за исключением некоторых незначительных отличий из-за встраивания. Но вы видите, что MSVC, похоже, перезагружает эту базу на каждой итерации: godbolt.org/z/66c5rJ

Christophe 27.10.2018 12:07

@markzzz: В исходном коде слишком много шума, чтобы точно сказать, что происходит. Единственное, что я могу посоветовать, - это инициализировать массивы вне вашей функции вычисления и передавать в нее уже инициализированные массивы / указатели / объекты: таким образом, разборка намного понятнее, функция отвечает только за одну вещь и не заботится о том, как массивы инициализирован.

Daniel Kamil Kozar 27.10.2018 12:07

@DanielKamilKozar, вы имеете в виду что-то вроде этого? coliru.stacked-crooked.com/a/0063855e4fdef0c8 в MSVC здесь возвращает ~ 80 мс на обоих, но я чувствую, что он выбрасывает массив (поэтому не вычисляйте его, так как оно слишком медленное время выполнения). Но странно то, что std :: cout выводит «правильный» результат, поэтому удалить их нельзя: O

markzzz 27.10.2018 12:12

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

Pezo 27.10.2018 12:15

Ваш измененный образец кода по-прежнему не является допустимым эталоном. Внешний цикл while (whileCounter < whileLoops) никак не влияет на результат. Также вы должны убедиться, что весь результат влияет на вывод (т. Е. Выводит все элементы).

zett42 27.10.2018 12:23
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
23
129
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вы компилируете с максимальным флагом оптимизации GCC, то есть O3, то вы получите аналогичное время выполнения.

Нет никакой разницы в аспекте выполнения функции внутри класса или вне его, w.r.t. время исполнения.


Единственное различие, которое я вижу, - это когда и как вы создаете свои массивы. В первой функции массивы являются автоматическими переменными функции. Во внутренней функции массивы являются членами данных класса.

В некоторых случаях это может сыграть роль. Сделайте массивы глобальными (создайте их только один раз), и вы не увидите разницы во времени выполнения (независимо от того, используете ли O1, O2 или O3).


Примечание: скомпилируйте с O2, и вы получите более быстрое время выполнения для внутренней функции (это наоборот, о чем вы упомянули). Если быть точным, то ускорение в 1,35 раза, как вы можете видеть в Live Demo.

Тем не менее, помните, что при правильной оптимизации, в данном случае с O3, вы не должны увидеть каких-либо существенных различий!

Это может быть разница в вычислении адресов памяти. Один рассчитывается относительно смещения местоположения объекта в стеке, что может быть немного сложнее, чем в случае простой функции. Также есть подозрительная для меня переменная double value в вашем цикле внутри класса, @markzzz. Это может повлиять на использование регистров или, в худшем случае, на дополнительные выделения.

muradm 27.10.2018 12:29

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

Pezo 27.10.2018 12:35

@Pezo, ага, я вижу, добавлены новые петли. не соответствовали им, извините за double value, забрав ту вещь, но не другую.

muradm 27.10.2018 12:38

Другие вопросы по теме