LINUX.ORG.RU

Что происходит в тестах на разных CPU?

 ,


1

1

Наткнулся на статью про сравнение языков: https://habr.com/ru/articles/532432/ Там на разных языках проверка числа на простое.

Получаю какие-то странные результаты (моя локальная машина и два разных VPS)

testAMD Ryzen 5 PRO 4650G (bogomips 7389.19)AMD EPYC 7763 (bogomips 4890.81)Intel Xeon CPU E5-2650 v2 (bogomips 5187.65)
go run test32.go4.154385519s1.823679616s3.120310686s
go run test64.go4.148815286s2.0692464s9.031017128s
node test.js4.1382.0365.387
./test-cpp4.16982 sec2.19747 sec3.18896 sec
  1. Почему рабочая машина так здорово проигрывает явно более слабым ЦПУ на VPS’ках? Энергосбережение? Не успевает разогнаться в MHz?

  2. Почему на процессорах AMD почти не играет роли int32/int64 в Go, а на Intel заметно играет?

★★★★

Последнее исправление: Toxo2 (всего исправлений: 1)

Про рабочую машину. Рискну предположить что дело в объёме кэш памяти. Окончательно будет понятно, если протестируете на рабочей машине, на другой частоте, что бы выявить на сколько она вообще влияет.

sv_warvar
()
Ответ на: комментарий от sv_warvar

Пока убрал amd_pstate=active из параметров ядра.

Был драйвер amd_pstate_epp и 400MHz в простое
После перезагрузки стал acpi-cpufreq и 1400MHz в простое.

На время исполнения тех тестов никак не повлияло - в районе 4 секунд.

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от MOPKOBKA

«Слабость» - она ж относительная. Вроде бы по всем пирогам должен «сильнее» тех, VPSных.

@sv_warvar

Вот что по кэшам выходит:

Caches (sum of all)РабочаяVPS-AMD-VMWareVPS-Intel-Xen
L1d:192 KiB (6 instances)64 KiB (2 instances)32 KiB (1 instance)
L1i:192 KiB (6 instances)64 KiB (2 instances)32 KiB (1 instance)
L2:3 MiB (6 instances)1 MiB (2 instances)256 KiB (1 instance)
L3:8 MiB (2 instances)64 MiB (2 instances)20 MiB (1 instance)

Т.е. получается это из-за мелкого кэша 3го уровня?

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от Toxo2

Коротко: тест ничего не тестирует.

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

Кеш тут роли не играет, т.к. нет никаких объемов, в каждый момент времени хранится только несколько чисел – которое проверяют на простоту, предполагаемый делитель и два счетчика циклов.

Поэтому тест будет упираться в то, как микрокод процессора оптимизирует поток инструкций (видимо в большей степени, т.к. результат работы функции не используется, циклы параллелятся и пр.) и в частоту проца. Можно посмотреть в какие инструкции превращает компилятор эту программу без оптимизации…

Вывод: никаких выводов сделать нельзя ввиду некорректности теста.

Для Ъ (код из ссылки):

#include <iostream>
#include <cmath>
#include <time.h>

using namespace std;

bool isPrime(int num)
{
    if (num == 2) {
        return true;
    }
    if (num <= 1 || num % 2 == 0) {
        return false;
    }

    double sqrt_num = sqrt(double(num));
    for (int div = 3; div <= sqrt_num; div +=2)
    {
        if (num % div == 0) {
            return false;
        }
    }
    return true;
}


int main()
{
    int N = 10000000;
    clock_t start, end;
    start = clock();
    for (int i = 0; i < N; i++) {
        isPrime(i);
    }
    end = clock();
    cout << (end - start) / ((double) CLOCKS_PER_SEC);
    cout << " sec \n";
    return 0;
}
soomrack ★★★★★
()
Последнее исправление: soomrack (всего исправлений: 2)
Ответ на: комментарий от Toxo2

Да, странно.

Я бы переделал слегка тест, чтобы оптимизатор при -O3 и др. оптимизациях не выкидывал функцию и цикл.

#include <iostream>
#include <cmath>
#include <time.h>

using namespace std;

bool isPrime(int num)
{
    if (num == 2) {
        return true;
    }
    if (num <= 1 || num % 2 == 0) {
        return false;
    }

    double sqrt_num = sqrt(double(num));
    for (int div = 3; div <= sqrt_num; div +=2)
    {
        if (num % div == 0) {
            return false;
        }
    }
    return true;
}


int main()
{
    int N = 10000000;
    clock_t start, end;
    int result = 0;
    start = clock();
    for (int i = 0; i < N; i++) {
        result+=(int)isPrime(i);
    }
    end = clock();
    cout << (end - start) / ((double) CLOCKS_PER_SEC);
    cout << " sec \n";
    cout<<result<<"\n";
    return 0;
}

Заодно результат чуточку полезнее становится - количество простых чисел выводится.

$ g++ -O0 test-cpp.cpp -o test-cpp-O0 && ./test-cpp-O0
4.34338 sec 
664579

$ g++ -O1 test-cpp.cpp -o test-cpp-O1 && ./test-cpp-O1
4.30605 sec

$ g++ -O2 test-cpp.cpp -o test-cpp-O1 && ./test-cpp-O2
4.30437 sec

g++ -O3 test-cpp.cpp -o test-cpp-O3 && ./test-cpp-O3
4.30343 sec

Проц Ryzen 9 3900X на постоянной частоте 4100 Мгц.

Простые оптимизации не влияют на скорость.

praseodim ★★★★★
()
Последнее исправление: praseodim (всего исправлений: 1)
Ответ на: комментарий от soomrack

никаких выводов сделать нельзя ввиду некорректности теста.

ну, почему же прям никаких?

две VPS с абсолютно одинаковыми Debian 12, полностью обновленные на сегодняшнее число
рабочая машина с ArchLinux

беру исходники, ничего с ними не делаю, никаких оптимизаций - AS IS.

и получаю результаты, которым не могу найти объяснение. Бог с ним, с С++, меня честно говоря больше всего Go взволновал.

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от praseodim

E5-2696 v3:

g++ -O3 2.cpp -o test-cpp-O3 && ./test-cpp-O3
2.76109 sec 

Xeon(R) CPU X5550:

g++ -O3 2.cpp -o test-cpp-O3 && ./test-cpp-O3
3.59535 sec

Intel(R) Core(TM) i3-4370:

g++ -O3 2.cpp -o test-cpp-O3 && ./test-cpp-O3
2.36739 sec

Ryzen 3 5300u:

g++ -O3 2.cpp -o test-cpp-O3 && ./test-cpp-O3
4.54539 sec 

Ну ХЗ, можно ли сравнивать процы друг с другом.

einhander ★★★★★
()
Последнее исправление: einhander (всего исправлений: 2)
Ответ на: комментарий от Toxo2

две VPS с абсолютно одинаковыми Debian 12,

VPS могут быть разные с разным хостом

никаких оптимизаций

оптимизация есть

  1. на уровне VPS (гипервизор),

  2. на уровне микрокода CPU,

  3. на уровне инструкций компилятора (флаг -march=native например для gcc), что там у тебя по умолчанию выставлено?

soomrack ★★★★★
()
Ответ на: комментарий от soomrack

У меня тоже. AMD тут явно заметно медленнее Intel. Оптимизация под AMD слегка ускоряет, но не принципиально.

$ g++ -Ofast test-cpp.cpp -o test-cpp-Ofast && ./test-cpp-Ofast
4.30136 sec 
664579

$ g++ -Ofast -march=native test-cpp.cpp -o test-cpp-Ofast-native && ./test-cpp-Ofast-native
4.29901 sec

$ g++ -Ofast -march=native -flto test-cpp.cpp -o test-cpp-Ofast-native-flto && ./test-cpp-Ofast-native-flto
4.29844 sec
praseodim ★★★★★
()
Ответ на: комментарий от praseodim

AMD тут явно заметно медленнее Intel

Тут все от архитектуры зависит, Zen 3 уже в 4 раза быстрее:

~/Documents/cpp ❯ g++ -O2 -march=native -o testO2 ./test.cpp && ./testO2
664579 prime count
1.07422 sec 
А Zen 4 подозреваю еще на ~40% быстрее будет на такой задаче.

arax ★★
()
Последнее исправление: arax (всего исправлений: 1)
Ответ на: комментарий от soomrack

Я думаю, что это как раз из-за микрокода CPU. Причем это может быть даже не оптимизация, а патчи против spectre и пр

влепил в параметры запуска ядра mitigations=off - ничегошеньки не поменялось

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от arax

Через cpupower смотрел?

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

#include <iostream>
#include <cmath>
#include <time.h>

using namespace std;


int main()
{
    clock_t start, end;
    // double result = 0.0;
    int result = 0;
    start = clock();
    for (int k1 = 1; k1 < 10000; k1++) {
        for (int k2 = 1; k2 < 100000; k2++) {
            // result += (double)k1 / (double)k2;
            result += k1 % k2;
        }
    }
    end = clock();
    cout << (end - start) / ((double) CLOCKS_PER_SEC);
    cout << " sec \n";
    cout<<result<<"\n";
    return 0;
}

AMD FX9590 4GHz

3.98982 sec 
855312446

Для double (раскомментить соотв. строчки)

4.65588 sec 
6.04446e+08
soomrack ★★★★★
()
Последнее исправление: soomrack (всего исправлений: 2)
Ответ на: комментарий от Toxo2

влепил в параметры запуска ядра mitigations=off - ничегошеньки не поменялось

А это влияет только на ядро и взаимодействие с ядром, т.е. системные вызовы. Защиту от mitigations добавляли в т.ч. и в микрокод…

soomrack ★★★★★
()
Ответ на: комментарий от Toxo2

Поработаю - попробую еще откатить amd-ucode на подальше.

Разумней все же смотреть на скорость выполения базовых операций. В этом раньше была ОЧЕНЬ большая разница между AMD и Intel, до процов серии FX. Но AMD тогда брали большей скорость работы с оперативой… Плюс стоит посмотреть на эти базовые операции с разным размером (int32, int64), т.к. очень может быть, что несколько операций проц умеет засовывать в одну инструкцию…

soomrack ★★★★★
()
:~$ cat /proc/cpuinfo  | grep 'model name' | tail -n1
model name      : Intel(R) Xeon(R) CPU           L5420  @ 2.50GHz
:~$ ./a.out
3.64062 sec
:~$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Ubuntu 18.04, WSL1 под Шindoшs 10

harbinger
()
Ответ на: комментарий от soomrack

Вроде как тут и есть намного быстрее

Нет намного быстрее это так:

~/Documents/cpp ❯ g++ -ltbb -O2 -march=native -o testThreads ./test.cpp && ./testThreads
32 threads
93ms
664579 prime count

#include <iostream>
#include <cmath>
#include <algorithm>
#include <execution>
#include <vector>
#include <chrono>
#include <thread>


using namespace std;

bool isPrime(int num)
{
    if (num == 2) {
        return true;
    }
    if (num <= 1 || num % 2 == 0) {
        return false;
    }

    double sqrt_num = sqrt(double(num));
    for (int div = 3; div <= sqrt_num; div +=2)
    {
        if (num % div == 0) {
            return false;
        }
    }
    return true;
}



int main()
{
    int N = 10000000;

    int num_phreads = std::thread::hardware_concurrency();
    std::cout << num_phreads << " threads\n";

    std::vector<int> it(num_phreads),res(num_phreads,0);
    std::iota (std::begin(it), std::end(it), 0);
    int K = N / num_phreads;
    auto begin = std::chrono::high_resolution_clock::now();

    std::for_each(std::execution::par_unseq, std::begin(it), std::end(it), [&](int j) {
    int start_num= j*K;
    int end_num = start_num+K;
    for (int i = start_num; i < end_num; i++) {
            if(isPrime(i)){
                res[j]++;
            }
        }
    });

    auto end = std::chrono::high_resolution_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end-begin).count() << "ms" << std::endl;

    cout << std::reduce(res.begin(), res.end());;
    cout << " prime count\n";
    return 0;
}
arax ★★
()
Последнее исправление: arax (всего исправлений: 1)
Ответ на: комментарий от arax

Какую-то я кривую говнокодину написал, вот нормальный вариант на openmp:

#include <iostream>
#include <cmath>
#include <chrono>
#include <omp.h>


using namespace std;

bool isPrime(int num)
{
    if (num == 2) {
        return true;
    }
    if (num <= 1 || num % 2 == 0) {
        return false;
    }

    double sqrt_num = sqrt(double(num));
    for (int div = 3; div <= sqrt_num; div +=2)
    {
        if (num % div == 0) {
            return false;
        }
    }
    return true;
}



int main()
{
    int N = 10000000;
    int c =0;

    #pragma omp parallel
    {
        #pragma omp single
        std::cout << omp_get_num_threads() << " threads\n";
    }

    auto begin = std::chrono::high_resolution_clock::now();

    #pragma omp parallel for reduction(+:c)
    for (int i = 0; i < N; i++) {
        c+=isPrime(i);
    }

    auto end = std::chrono::high_resolution_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end-begin).count() << "ms" << std::endl;

    cout << c;
    cout << " prime count\n";
    return 0;
}

Работает еще быстрее:

~/Documents/cpp ❯ g++ -fopenmp -O2 -march=native -o test-omp ./test-omp.cpp &&OMP_NUM_THREADS=512 ./test-omp
512 threads
70ms
664579 prime count

arax ★★
()
Ответ на: комментарий от arax

Горшочек не вари.
Но я не мог остановиться, если слегка оптимизировать алгоритм проверки, то получаем так:

~/Documents/cpp ❯ g++ -fopenmp -O2 -march=native -o test-omp ./test-omp.cpp &&OMP_NUM_THREADS=512 ./test-omp
512 threads
47ms
664579 prime count

#include <iostream>
#include <chrono>
#include <omp.h>

bool isPrime(int x)
{
    if(x <= 1) return false;
    if(x == 2 || x == 3) return true;
    if(x % 2 == 0 || x % 3 == 0) return false;
    if((x - 1) % 6 != 0 && (x + 1) % 6 != 0) return false;
    for(int i = 5; i * i <= x; i += 6) if(x % i == 0 || x % (i + 2) == 0) return false;
    return true;
}

int main()
{
    int N = 10000000;
    int prime_count =0;

    #pragma omp parallel
    #pragma omp single
    std::cout << omp_get_num_threads() << " threads\n";

    auto begin = std::chrono::high_resolution_clock::now();

    #pragma omp parallel for reduction(+:prime_count)
    for (int i = 0; i < N; i++) prime_count+=isPrime(i);

    auto end = std::chrono::high_resolution_clock::now();

    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end-begin).count() << "ms" << std::endl;
    std::cout << prime_count << " prime count" << std::endl;
    return 0;
}
arax ★★
()
Ответ на: комментарий от arax

На изначальных машинах из шапки ваш этот код делает так:

рабочая: 12 threads 783ms
VPS-AMD: 2 threads 1110ms
VPS-Intel: 2 threads 1696ms
как бы 12 лучше, чем 2, но не в шесть раз )

---

а так, если вернуться к однопоточному оригиналу - я уж и amd_ucode двигал в Arch, и в Void с musl перезагружался и там собирал С++ и запускал в Go - хоть тресни 4 секунды получается на рабочей машине.

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от Toxo2

С одной стороны это вообще не тест, а с другой подозрительно медленно.
Я бы на вашем месте, загрузился с livecd с виндой и там в аиде потестил, а то может у вас что нибудь с железом.

я уж и amd_ucode двигал в Arch

Я тут уже писал что там нет микрокода для десктопных процессоров и его можно просто удалить.

arax ★★
()
Последнее исправление: arax (всего исправлений: 1)
Ответ на: комментарий от arax

А что с мои примером, когда просто смотрим как быстро деление выполняют AMD или Intel?

PS: чудес с распараллеливанием не бывает, то оптимизатор проца может некоторые бранчи параллельно считать, в чем и была причина первых проблем класса спектр…

soomrack ★★★★★
()
Ответ на: комментарий от arax

Ага.

EPYC 7662: Max. Boost Clock Up to 3.3GHz Base Clock 2.0GHz

4.25124 sec 
855312446

1.51601 sec 
6.04446e+08

Можно сказать, что AMD работу с double сильно ускорила в zen2 по сравнению с временами FX, а целочисленное деление не изменилось…

soomrack ★★★★★
()
Последнее исправление: soomrack (всего исправлений: 1)
Ответ на: комментарий от soomrack

Поиграл ещё немного в эту игру.

целочисленное деление не изменилось…

Пока только в этом месте некоторое понимание пришло. Действительно что-то происходит вокруг idiv

такое:

        mov     rax, QWORD PTR [rbp-24]
        cqo
        idiv    QWORD PTR [rbp-8]
против такого:
        mov     eax, DWORD PTR [rbp-20]
        cdq
        idiv    DWORD PTR [rbp-4]
для AMD процессоров почти ничего не меняет. А вот для Intel - прям в разы быстрее, когда оно в 32 битах.

Осталось найти почему мой AMD Ryzen 5 PRO 4650G сильно медленнее чем AMD EPYC 7763. Кроме кэша третьего уровня - всё у него лучше. И память проверил - тоже быстрее.

Toxo2 ★★★★
() автор топика
Ответ на: комментарий от Toxo2

Осталось найти почему мой AMD Ryzen 5 PRO 4650G сильно медленнее чем AMD EPYC 7763

4650G это ZEN2, а 7763 - ZEN3, разница в архитектуре на этой задаче дает ускорение в 4 раза на одной частоте.
Тут уже несколько раз приводили результаты ZEN2 и они соответствуют вашим.

arax ★★
()
Ответ на: комментарий от arax

Точно. Спасибо.

Вот что нашел: https://www.agner.org/optimize/instruction_tables.pdf

Там так:

Zen2
IDIV r8/m8   1 12-15 12-15
IDIV r16/m16 2 13-20 13-20
IDIV r32/m32 2 13-28 13-28
IDIV r64/m64 2 13-44 13-44

Zen3
IDIV r8/m8   2 11    4
IDIV r16/m16 2 11-12 4
IDIV r32/m32 2 9-12  6
IDIV r64/m64 2 9-17  7-12
я так понимаю - в 4 раза быстрее вполне похоже на правду

Toxo2 ★★★★
() автор топика
Последнее исправление: Toxo2 (всего исправлений: 1)