Не уверен, какой вид оптимизации выполняет 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;
Те же результаты ...
@Christophe: да, как я уже сказал, /Oi /Ot
А / O2, все тот же?
Я вижу различия в распределении памяти. В глобальной функции вы делаете это внутри тела функции, в классах массивы заранее выделяются заранее. Падение в разные места, в зависимости от вашей машины / нагрузки, время доступа может быть разным. Сравнивать их все равно, что сравнивать яблоки с виноградом.
Вывод на моем компьютере: execution time: 9647 ms | fake result: 19 1090 1258 execution time: 9565 ms | fake result: 19 1090 1258
. Я использую компилятор gcc, Ubuntu 16.04.
Я получаю аналогичные тайминги в 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
Звонок system()
, правда? Почему? Кстати, я вижу только 1.4 ускорение, но все же .. С O2
.
@muratm, что неверно, массивы выделяются в стеке в обоих случаях (как локальные переменные в одном случае, как члены локальной переменной в другом случае).
С массивами с нулевой инициализацией: 74 мс против 1024 мс. Бьюсь об заклад, компилятор оптимизирует большинство циклов, поскольку наблюдаемый эффект не зависит от всего результата.
@ Мат, который может быть причиной. В случае, если все в одной функции, компилятор видит, что массивы не инициализированы, и удаляет все, что с ними связано. В случае функции-члена это невозможно, потому что он не может видеть, были ли массивы инициализированы где-то еще до вызова функции-члена.
@Pezo, да, верно пропустил / предположил OptimizeProcess p1
.
@Pezo: если я инициализирую оба массива в обеих функциях, ничего не изменится (все равно 80 против 1200 мс ~).
Странно, я сейчас на мобильном телефоне, поэтому не могу проверить, извините.
Компиляция одного и того же слегка измененный код (удаление iostream / chrono и создание массивов глобальными, чтобы компилятор не оптимизировал их) на Godbolt показывает, что для обеих версий генерируется один и тот же код. Таким образом, дальнейшее обсуждение бессмысленно.
О, кстати, вы смотрели сгенерированную сборку? Может быть, опубликуйте ссылку Godbolt.
@muradm: я думаю, что это должно быть как Pezo, но если я сделаю так, как вы предлагаете (т.е. переместите массив / init в основной и передайте их как указатель, оба будут около 80 мс): O Очень странно
@DanielKamilKozar: как вы "инициализируете" массивы? В основном так? Можете показать весь пример? Не уверен, почему, если я инициализирую функцию, она должна ее удалить: O
@DanielKamilKozar: смотрит мой обновленный ответ с массивами inits: на самом деле он их не убирает (поскольку std :: cout выводит правильные значения). Так что все еще не уверен, что происходит ...
Здесь вы можете видеть, что gcc почти генерирует один и тот же код (за исключением некоторых незначительных отличий из-за встраивания. Но вы видите, что MSVC, похоже, перезагружает эту базу на каждой итерации: godbolt.org/z/66c5rJ
@markzzz: В исходном коде слишком много шума, чтобы точно сказать, что происходит. Единственное, что я могу посоветовать, - это инициализировать массивы вне вашей функции вычисления и передавать в нее уже инициализированные массивы / указатели / объекты: таким образом, разборка намного понятнее, функция отвечает только за одну вещь и не заботится о том, как массивы инициализирован.
@DanielKamilKozar, вы имеете в виду что-то вроде этого? coliru.stacked-crooked.com/a/0063855e4fdef0c8 в MSVC здесь возвращает ~ 80 мс на обоих, но я чувствую, что он выбрасывает массив (поэтому не вычисляйте его, так как оно слишком медленное время выполнения). Но странно то, что std :: cout выводит «правильный» результат, поэтому удалить их нельзя: O
@DanielKamilKozar, вы полностью изменили семантику кода, поэтому различия неизбежны. Это просто больше не полезно для рассуждений об исходном примере.
Ваш измененный образец кода по-прежнему не является допустимым эталоном. Внешний цикл while (whileCounter < whileLoops)
никак не влияет на результат. Также вы должны убедиться, что весь результат влияет на вывод (т. Е. Выводит все элементы).
Если вы компилируете с максимальным флагом оптимизации GCC, то есть O3
, то вы получите аналогичное время выполнения.
Нет никакой разницы в аспекте выполнения функции внутри класса или вне его, w.r.t. время исполнения.
Единственное различие, которое я вижу, - это когда и как вы создаете свои массивы. В первой функции массивы являются автоматическими переменными функции. Во внутренней функции массивы являются членами данных класса.
В некоторых случаях это может сыграть роль. Сделайте массивы глобальными (создайте их только один раз), и вы не увидите разницы во времени выполнения (независимо от того, используете ли O1
, O2
или O3
).
Примечание: скомпилируйте с O2
, и вы получите более быстрое время выполнения для внутренней функции (это наоборот, о чем вы упомянули). Если быть точным, то ускорение в 1,35 раза, как вы можете видеть в Live Demo.
Тем не менее, помните, что при правильной оптимизации, в данном случае с O3
, вы не должны увидеть каких-либо существенных различий!
Это может быть разница в вычислении адресов памяти. Один рассчитывается относительно смещения местоположения объекта в стеке, что может быть немного сложнее, чем в случае простой функции. Также есть подозрительная для меня переменная double value
в вашем цикле внутри класса, @markzzz. Это может повлиять на использование регистров или, в худшем случае, на дополнительные выделения.
@muradm эта переменная присутствует в обоих случаях, ее, безусловно, следует оптимизировать и в обоих случаях.
@Pezo, ага, я вижу, добавлены новые петли. не соответствовали им, извините за double value
, забрав ту вещь, но не другую.
Вы компилировали с включенной оптимизацией? Потому что здесь я получаю почти одинаковую цифру для обоих исполнений: ideone.com/uZK787