Подтвердить что ты не робот

Как несколько сравнений могут быть медленнее, чем некоторые вычисления?

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

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

Учитывая следующий код:

#include <iostream>
#include <chrono>

using namespace std;
using namespace std::chrono;

struct timing {
    short hour;
    short minute;
};

bool isAllowed(timing &from, timing &to, timing &actual) {
    return !(((from.hour > to.hour && (actual.hour >= from.hour || actual.hour <= to.hour)) ||
        (actual.hour >= from.hour && actual.hour <= to.hour)) &&
        !(actual.minute > from.minute && actual.minute < to.minute));
}

long getSecs(short hour, short minutes) {

    return (hour * 3600) + (minutes * 60);

}

bool isAllowed2(timing &from, timing &to, timing &current) {

    long secsFrom = getSecs(from.hour, from.minute);
    long secsTo = getSecs(to.hour, to.minute);
    long secsCurrent = getSecs(current.hour, current.minute);

    if (secsFrom > secsTo) secsTo += 24 * 60 * 60;
    if (secsCurrent > secsFrom && secsCurrent < secsTo) {
        return false;
    }

    return true;
}

int main() {
    //debug messages
    std::string okay = " - ok";
    std::string error = " - er";

    std::string receive = " - allowed";
    std::string notReceive = " - denied";

    //testing times
    int const testDataCount = 5;
    timing from[testDataCount] = {
        { 16, 30 },
        { 8,  30 },
        { 10, 30 },
        { 0, 30 },
        { 0, 0 }
    };
    timing to[testDataCount] = {
        { 8,  30 },
        { 20, 0 },
        { 20, 0 },
        { 6, 0 },
        { 7, 0 }
    };

    for (int i = 0; i < testDataCount; i++) {
        std::cout << i + 1 << ": " << from[i].hour << ":" << from[i].minute << " to " << to[i].hour << ":"
            << to[i].minute << std::endl;
    }

    //test current times
    timing current[5] = {
        { 12, 0 },
        { 23, 0 },
        { 17, 30 },
        { 15, 12 },
        { 0, 20 }
    };

    bool ergValues[][testDataCount] = {
        { true,  false, false, true, true },
        { false, true,  true, true, true },
        { false, false, false, true, true },
        { true,  false, false, true, true },
        { false,  true, true, true, false }
    };

    long totalNs1 = 0;
    long totalNs2 = 0;

    for (int i = 0; i < 4; i++) {
        std::cout << std::endl << i + 1 << ". Test: " << current[i].hour << ":" << current[i].minute << std::endl;
        for (int j = 0; j < testDataCount; j++) {

            high_resolution_clock::time_point t1 = high_resolution_clock::now();
            bool response = isAllowed(from[j], to[j], current[i]);
            high_resolution_clock::time_point t2 = high_resolution_clock::now();

            high_resolution_clock::time_point t3 = high_resolution_clock::now();
            bool response2 = isAllowed2(from[j], to[j], current[i]);
            high_resolution_clock::time_point t4 = high_resolution_clock::now();

            long ns1 = duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
            totalNs1 += ns1;
            long ns2 = duration_cast<std::chrono::nanoseconds>(t4 - t3).count();
            totalNs2 += ns2;

            std::cout << j + 1 << "\t\t:1:" << ns1 << "ns: " << response << (response == ergValues[i][j] ? okay : error) << "\t\t:2:" << ns2 << "ms: " << response2 << (response2 == ergValues[i][j] ? okay : error) << "\t\t"
                << (ergValues[i][j] ? receive : notReceive) << std::endl;
        }
    }

    std::cout << "\r\ntotalNs1 = " << totalNs1 << "\r\ntotalNs2 = " << totalNs2 << "\r\n\r\n";

    return 0;
}

Результат, очевидно, всегда будет отличаться, но независимо от того, что totalNs2 всегда будет меньше, чем totalNs1.

Пример:

totalNs1 = 38796
totalNs2 = 25913

Я тестировал это на AMD Phenom II X4 и Intel i7-3770, как на Windows 10, так и на Debian 8, с аналогичными результатами.

Итак, наконец, вопрос в том, почему функция isAllowed2 быстрее, чем isAllowed?

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


ИЗМЕНИТЬ

Тем временем я изучал дальше, основываясь на всех предложениях и комментариях, включая этот > невероятно подробный ответ, понимая, насколько может быть неточным микро-бенчмаркинг (ОГРОМНЫЙ спасибо Baum mit Augen за ссылку этот удивительный разговор, который очень помог) Я закончил использование Google microbenchmark library, чтобы получить более "точные" результаты, кажется, что функция isAllowed на самом деле быстрее (скомпилирована без оптимизации), как показано на выходе из библиотеки.

Run on (8 X 2395 MHz CPU s)
-----------------------------------------------------------------------
Benchmark                                Time           CPU Iterations
-----------------------------------------------------------------------
BM_isAllowed/2/min_time:2.000           22 ns         22 ns  128000000
BM_isAllowed/4/min_time:2.000           22 ns         22 ns  137846154
BM_isAllowed/8/min_time:2.000           22 ns         22 ns  128000000
BM_isAllowed/16/min_time:2.000          22 ns         22 ns  128000000
BM_isAllowed/22/min_time:2.000          22 ns         22 ns  137846154
BM_isAllowed2/2/min_time:2.000          24 ns         24 ns  112000000
BM_isAllowed2/4/min_time:2.000          24 ns         24 ns  119466667
BM_isAllowed2/8/min_time:2.000          24 ns         24 ns  119466667
BM_isAllowed2/16/min_time:2.000         24 ns         24 ns  119466667
BM_isAllowed2/22/min_time:2.000         24 ns         24 ns  119466667

Примечание. Как отметил Мартин Боннер, функция isAllowed, похоже, имеет логический недостаток, не используйте его производственный код.

Ниже вы можете найти код, который я использовал для этого теста, сообщите мне, есть ли в нем недостатки, поскольку я не знаком с Библиотека Google.

Важно, этот код был скомпилирован с Visual Studio 2015, и оптимизация должна быть отключена для раздела, который мы хотим проверить.

#include "benchmark/benchmark.h"

using namespace std;
using namespace benchmark;

#pragma optimize( "[optimization-list]", {on | off} )

volatile const long extraDay = 24 * 60 * 60;

#pragma optimize( "", off )

struct timing {
    short hour;
    short minute;
    timing(short hour, short minute) : hour(hour), minute(minute) {}
};

static void BM_isAllowed(benchmark::State& state) {

    while (state.KeepRunning())
    {
        timing from(state.range(0), state.range(0));
        timing to(state.range(0), state.range(0));
        timing current(state.range(0), state.range(0));

        bool b = !(((from.hour > to.hour && (current.hour >= from.hour || current.hour <= to.hour)) ||
            (current.hour >= from.hour && current.hour <= to.hour)) &&
            !(current.minute > from.minute && current.minute < to.minute));
    }
}

static void BM_isAllowed2(benchmark::State& state) {

    while (state.KeepRunning())
    {
        timing from(state.range(0), state.range(0));
        timing to(state.range(0), state.range(0));
        timing current(state.range(0), state.range(0));

        bool b;
        long secsFrom = secsFrom = (from.hour * 3600) + (from.minute * 60);
        long secsTo = (to.hour * 3600) + (to.minute * 60);
        long secsCurrent = (current.hour * 3600) + (current.minute * 60);

        if (secsFrom > secsTo)
            secsTo += extraDay;
        if (secsCurrent > secsFrom && secsCurrent < secsTo)
            b = false;
        else
            b = true;
    }
}
#pragma optimize( "", on ) 

BENCHMARK(BM_isAllowed)->RangeMultiplier(2)->Range(2, 22)->MinTime(2);
BENCHMARK(BM_isAllowed2)->RangeMultiplier(2)->Range(2, 22)->MinTime(2);
BENCHMARK_MAIN();
4b9b3361

Ответ 1

Для этого нет золотого правила. К сожалению, производительность такого кода, как известно, трудно предсказать. Самое главное, что можно отнять у него:

Измерьте все!

Теперь, что происходит в вашем коде: как было отмечено другими, мы можем заметить, что isAllowed скомпилируется в функцию с помощью ветвей, а isAllowed2 заканчивается безветкистым.

Филиалы интересны, когда говорят о производительности: они где-то между буквально свободными и смехотворно дорогими, в том числе. Это связано с компонентом процессора, называемым предиктором ветвления. Он пытается предсказать, какая ветвь будет обрабатываться вашим потоком управления, и процессор будет спекулятивно выполнять его. Если он догадывается правильно, ветка свободна. Если он ошибается, ветка стоит дорого. Большое и подробное объяснение этой концепции, включая некоторые цифры, можно найти в этом ответе.

Итак, теперь нам нужно решить, хотим ли мы разветвления или безветровая версия. В общем, ни одна из них не должна быть быстрее, чем другая!. Это действительно зависит от того, насколько хорошо ваши целевые процессоры могут предсказать ветки, что, конечно, зависит от фактического ввода. (Выбор компиляции функции для ветвления или безветрового результата, таким образом, является трудной проблемой для компиляторов, поскольку они не знают, на каких процессорах будет работать двоичный файл, и каких входных данных ожидать. См. Например этот blogpost.)

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


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

Кроме того, как заметил Мартин Боннер , ваши две функции не делают то же самое, вам нужно будет исправить это для правильного теста.

Ответ 2

Поскольку вы не измеряете то, что хотите измерить.

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

Вы не выполняете требуемое измерение, потому что: 1. Время выполнения только после того, как эти функции имеют один и тот же порядок (даже меньше), чем время выполнения функции, которая фактически получает часы. Как правило, на большом пальце, чтобы проверить, попробуйте измерить время, превышающее 10 мс. 2. Между двумя тиками компилятор разместил агрессивно встроенные и оптимизированные версии ваших функций, потому что он знает, каковы входные данные, что, вероятно, не произойдет в реальном случае.

Если вы поместите определение своих двух функций в другой файл, кроме файла, в котором указаны ваши входы:

//is_allowed.cpp
struct timing {
    short hour;
    short minute;
};
bool isAllowed(timing &from, timing &to, timing &actual) {
    return !(((from.hour > to.hour && (actual.hour >= from.hour || actual.hour <= to.hour)) ||
        (actual.hour >= from.hour && actual.hour <= to.hour)) &&
        !(actual.minute > from.minute && actual.minute < to.minute));
}

static long getSecs(short hour, short minutes) {

    return (hour * 3600) + (minutes * 60);

}

bool isAllowed2(timing &from, timing &to, timing &current) {

    long secsFrom = getSecs(from.hour, from.minute);
    long secsTo = getSecs(to.hour, to.minute);
    long secsCurrent = getSecs(current.hour, current.minute);

    if (secsFrom > secsTo) secsTo += 24 * 60 * 60;
    if (secsCurrent > secsFrom && secsCurrent < secsTo) {
        return false;
    }

    return true;
}

И затем выполните миллион раз ваших функций между "тиками", вы получите гораздо более надежный результат:

int main(){
//...

            high_resolution_clock::time_point t1 = high_resolution_clock::now();
            for (int x=1;x<1000000;++x)
               isAllowed(from[j], to[j], current[i]);
            high_resolution_clock::time_point t2 = high_resolution_clock::now();

            high_resolution_clock::time_point t3 = high_resolution_clock::now();
            for (int x=1;x<1000000;++x)
               isAllowed2(from[j], to[j], current[i]);
            high_resolution_clock::time_point t4 = high_resolution_clock::now();

            long ns1 = duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
            totalNs1 += ns1;
            long ns2 = duration_cast<std::chrono::nanoseconds>(t4 - t3).count();
            totalNs2 += ns2;

//            std::cout << j + 1 << "\t\t:1:" << ns1 << "ns: " << response << (response == ergValues[i][j] ? okay : error) << "\t\t:2:" << ns2 << "ms: " << response2 << (response2 == ergValues[i][j] ? okay : error) << "\t\t"
//                << (ergValues[i][j] ? receive : notReceive) << std::endl;
//...
    }

Сюрприз, я получаю:

    totalNs1=38085793  //(38ms)
    totalNs2=52182920  //(52ms)

Пока с вашим кодом с тем же компьютером и компилятором я получил:

    totalNs1 = 927
    totalNs2 = 587

Как вы ожидали, первая версия isAllowed на самом деле является победителем!