Информация об изменениях

Сообщение Re[37]: benchmark от 10.01.2017 13:27

Изменено 10.01.2017 13:33 lpd

Re[37]: benchmark
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Ладно, вместо тысячи слов — встречайте её могущество копипаста без boost'а. Соотношения получились примерно те же самые


1) По твоему тесту:
Во-первых, как я понял, ты пытаешься измерить задержки из-за cache-miss с помощью listа. Думаю, что это не очень хороший подоход, т.к. элементы list могут быть расположены в памяти последовательно. В моем же тесте, я точно обращаюсь к элементам в случайном порядке.
Во-вторых, ты измеряешь производительность stl контейнеров, а не процессора.
2) По моему тесту:
В моем измерении тоже были ошибки: я компилировал без оптимизатора, а когда подключил оптимизатор он выкинул весь расчет. В конце поста я приведу для конкретности последний вариант своего кода.
Результаты изменились:
Последовательное сложение с индирекцией оказалось ровно в два раза медленне, чем с индирекцией. Это ожидаемо, т.к. выполняется две инструкции, вместо одной.
Сложение со случайным доступом(и вечным cache-miss) оказалось еще в два раза медленнее, чем с последовательным. Это обозначает верхнюю границу эффективности кэша при доступе к данным, и для меня интересно.
Предлагаю на этом результате остановиться, т.к. он меряет именно скорость процессора, а не скорость контейнеров stl. Кстати, только vector<int> оказался по скорости близок к массивам, а в остальных вариантах при одном, по сути, алгоритме твоя программа с stl, действительно, работает на порядки медленнее моей при одном размере массива(что удивило даже меня).

Относительно причин задержек Java/C# vs C++ думаю, что лишний код присущ JIT-компиляторам, и лишние индирекции часть этой проблемы. Однако, программируя на C++, очень редко есть смысл заботиться об индирекции, т.к. далеко не все функции в программе занимают значительное время. И алгоритм работы(особенно не относящийся к вычислительным) почти никогда не состоит из одной инструкции сложения чисел, поэтому обращения к памяти вносят меньший вклад во время выполнения, чем в этих синтетических тестах. Поэтому для меня лично кэш-миссы и индирекции недостаточная причина всегда использовать move-семантику для каждого вектора больших объектов(т.к. это слишком мелкая микрооптимизация, усложняющая код), а при тяжелых вычислениях я постараюсь обойтись без stl.

Привожу мой тест в последнем варианте:
#include <iostream>
#include <chrono>
#include <stdlib.h>
using namespace std;

#define SIZE 4000*1000
int *array;
int *map1, *map2;
int test(int *map) 
{
        int sum = 0;
        for (int i=0; i<SIZE; i++)
                sum += array[map[i]];
        return sum;
}
int test0()
{
        int sum = 0;
        for (int i=0; i<SIZE; i++)
                sum += array[i];
        return sum;
}
int main()
{
        srand(time(0));
        array = new int[SIZE];
        map1 = new int[SIZE];
        for (int i=0;i<SIZE;i++) {
                map1[i] = i;
                array[i] = random();
        }
        map2 = new int[SIZE];
        for (int i=0;i<SIZE;i++)
                map2[i] = random()%SIZE;
        auto start0 = std::chrono::high_resolution_clock::now();
        int r = test0();
        auto finish0 = std::chrono::high_resolution_clock::now();
        cout << r << ":" <<std::chrono::duration_cast<std::chrono::nanoseconds>(finish0-start0).count() << "ns\n";

        auto start1 = std::chrono::high_resolution_clock::now();
        r = test(map1);
        auto finish1 = std::chrono::high_resolution_clock::now();
        cout << r << ":" << std::chrono::duration_cast<std::chrono::nanoseconds>(finish1-start1).count() << "ns\n";

        auto start2 = std::chrono::high_resolution_clock::now();
        r = test(map2);
        auto finish2 = std::chrono::high_resolution_clock::now();
        cout << r << ":" << std::chrono::duration_cast<std::chrono::nanoseconds>(finish2-start2).count() << "ns\n";
}
Re[37]: benchmark
Здравствуйте, Evgeny.Panasyuk, Вы писали:

EP>Ладно, вместо тысячи слов — встречайте её могущество копипаста без boost'а. Соотношения получились примерно те же самые


1) По твоему тесту:
Во-первых, как я понял, ты пытаешься измерить задержки из-за cache-miss с помощью listа. Думаю, что это не очень хороший подоход, т.к. элементы list могут быть расположены в памяти последовательно. В моем же тесте, я точно обращаюсь к элементам в случайном порядке.
Во-вторых, ты измеряешь производительность stl контейнеров, а не процессора.
2) По моему тесту:
В моем измерении тоже были ошибки: я компилировал без оптимизатора, а когда подключил оптимизатор он выкинул весь расчет. В конце поста я приведу для конкретности последний вариант своего кода.
Результаты изменились:
Последовательное сложение с индирекцией оказалось ровно в два раза медленне, чем с индирекцией. Это ожидаемо, т.к. выполняется две инструкции, вместо одной.
Сложение со случайным доступом(и вечным cache-miss) оказалось еще в два раза медленнее, чем с последовательным. Это обозначает верхнюю границу эффективности кэша при доступе к данным, и для меня интересно.
Предлагаю на этом результате остановиться, т.к. он меряет именно скорость процессора, а не скорость контейнеров stl. Кстати, только vector<int> оказался по скорости близок к массивам, а в остальных вариантах при одном, по сути, алгоритме твоя программа с stl, действительно, работает на порядки медленнее моей при одном размере массива(что удивило даже меня).

Относительно причин задержек Java/C# vs C++ думаю, что лишний код присущ JIT-компиляторам, и лишние индирекции часть этой проблемы. Однако, программируя на C++, очень редко есть смысл заботиться об индирекции, т.к. далеко не все функции в программе занимают значительное время. И алгоритм работы(особенно не относящийся к вычислительным) почти никогда не состоит из одной инструкции сложения чисел, поэтому обращения к памяти вносят меньший вклад во время выполнения, чем в этих синтетических тестах. Поэтому для меня лично кэш-миссы и индирекции недостаточная причина всегда использовать move-семантику для каждого вектора больших объектов(т.к. это слишком мелкая микрооптимизация, усложняющая код) и все же буду использовать указатели, а при тяжелых вычислениях я постараюсь обойтись без stl.

Привожу мой тест в последнем варианте:
#include <iostream>
#include <chrono>
#include <stdlib.h>
using namespace std;

#define SIZE 4000*1000
int *array;
int *map1, *map2;
int test(int *map) 
{
        int sum = 0;
        for (int i=0; i<SIZE; i++)
                sum += array[map[i]];
        return sum;
}
int test0()
{
        int sum = 0;
        for (int i=0; i<SIZE; i++)
                sum += array[i];
        return sum;
}
int main()
{
        srand(time(0));
        array = new int[SIZE];
        map1 = new int[SIZE];
        for (int i=0;i<SIZE;i++) {
                map1[i] = i;
                array[i] = random();
        }
        map2 = new int[SIZE];
        for (int i=0;i<SIZE;i++)
                map2[i] = random()%SIZE;
        auto start0 = std::chrono::high_resolution_clock::now();
        int r = test0();
        auto finish0 = std::chrono::high_resolution_clock::now();
        cout << r << ":" <<std::chrono::duration_cast<std::chrono::nanoseconds>(finish0-start0).count() << "ns\n";

        auto start1 = std::chrono::high_resolution_clock::now();
        r = test(map1);
        auto finish1 = std::chrono::high_resolution_clock::now();
        cout << r << ":" << std::chrono::duration_cast<std::chrono::nanoseconds>(finish1-start1).count() << "ns\n";

        auto start2 = std::chrono::high_resolution_clock::now();
        r = test(map2);
        auto finish2 = std::chrono::high_resolution_clock::now();
        cout << r << ":" << std::chrono::duration_cast<std::chrono::nanoseconds>(finish2-start2).count() << "ns\n";
}