Производительность std::bind (GCC 4.7.1)
От: SkyDance Земля  
Дата: 12.09.13 06:21
Оценка:
Где-то пару-тройку лет назад проверял performance hit при переходе от free functions к виртуальным методам класса, и затем к boost::function в связке с boost::bind (такая замена сильно упрощала код). На тот момент замедление было существенным — вместо 1 нс на вызов простой функции уходило 20 нс (gcc 4.2 или как-то так).
К сожалению, код того теста остался на том месте работы.
Набросал новый, примерно такой (разумеется, непортабельный):

  Скрытый текст
#include <time.h>
#include <stdio.h>
#include <memory>
#include <functional>
#include <string.h>

__attribute__((noinline)) static int static_call(volatile char* _line)
{
    int count = 0;
    while (_line[count])
        count++;
    return count;
}

class cls
{
public:
    virtual ~cls(){};

    __attribute__((noinline)) static int static_class_call(volatile char* _line)
    {
        int count = 0;
        while (_line[count])
            count++;
        return count;
    }

    __attribute__((noinline)) int dynamic_class_call(volatile char* _line)
    {
        int count = 0;
        while (_line[count])
            count++;
        return count;
    }

    __attribute__((noinline)) virtual int virtual_class_call(volatile char* _line)
    {
        int count = 0;
        while (_line[count])
            count++;
        return count;
    }
};

int main()
{
    volatile char line[512];
    strcpy((char*)line, "123456789012345678901234567890123456789012345678901234567890");
    const int repetitions = 100000000; // 1 million
    int counter = 0;
    long long ns_diff;

    struct timespec clock_start, clock_end;

    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
    {
        counter = 0;
        while (line[counter])
            counter++;
    }
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
                clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - single strlen (%lld ns)\n", ns_diff, ns_diff / repetitions);

    // Test: function speed call
    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
        counter += static_call(line);
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
            clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - static function calls (%lld ns)\n", ns_diff, ns_diff / repetitions);

    cls obj;

    // static class method
    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
        counter += cls::static_class_call(line);
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
            clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - static class method calls (%lld ns)\n", ns_diff, ns_diff / repetitions);

    // class method
    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
        counter += obj.dynamic_class_call(line);
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
            clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - class method calls (%lld ns)\n", ns_diff, ns_diff / repetitions);

    // virtual class method
    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
        counter += obj.virtual_class_call(line);
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
            clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - virtual class method calls (%lld ns)\n", ns_diff, ns_diff / repetitions);

    // bound function call
    auto fn = std::bind(&cls::dynamic_class_call, obj, std::placeholders::_1);
    counter = 0;
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_start);
    for (int i=0; i<repetitions; i++)
        counter += fn(line);
    clock_gettime(CLOCK_MONOTONIC_RAW, &clock_end);
    ns_diff = (long long)(clock_end.tv_sec - clock_start.tv_sec) * 1000000000 +
            clock_end.tv_nsec - clock_start.tv_nsec;
    printf("%lld ns - bound method calls (%lld ns)\n", ns_diff, ns_diff / repetitions);

    return 0;
}


И результаты меня даже слегка удивили.
Во-первых, вызов виртуального метода стабильно быстрее вызова обычного. Во-вторых, начиная с длины строки где-то в размер одной строки кэша (16 символов) разницы с std::bind нет. Да и в целом std::function + std::bind чертовски быстры, даже на пустой строке дают не более 10% замедления.
В общем, я заподозрил, что где-то в чем-то ошибся в тесте. Часы (clock) — HPET, т.е. замеры корректны. Опять же, понимаю, что тут упирается в работу с памятью, но все равно уж больно мал порядок расхождения. Проверял на рвзных машинах, соотношение примерно одинаковое везде.
Может, кто-нибудь мне подскажет, где найти более осмысленные тесты? Интересует сравнение free function vs. virtual function vs. functor vs. std::function + std::bind.
Re: Производительность std::bind (GCC 4.7.1)
От: saf_e  
Дата: 12.09.13 07:42
Оценка:
Здравствуйте, SkyDance, Вы писали:

SD>Во-первых, вызов виртуального метода стабильно быстрее вызова обычного. Во-вторых, начиная с длины строки где-то в размер одной строки кэша (16 символов) разницы с std::bind нет. Да и в целом std::function + std::bind чертовски быстры, даже на пустой строке дают не более 10% замедления.

SD>В общем, я заподозрил, что где-то в чем-то ошибся в тесте. Часы (clock) — HPET, т.е. замеры корректны. Опять же, понимаю, что тут упирается в работу с памятью, но все равно уж больно мал порядок расхождения. Проверял на рвзных машинах, соотношение примерно одинаковое везде.
SD>Может, кто-нибудь мне подскажет, где найти более осмысленные тесты? Интересует сравнение free function vs. virtual function vs. functor vs. std::function + std::bind.

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

Ну а если все-таки у вас нагружненный, то имеет смысл изучать конкретную ситуацию, а не анализировать абстрактный call в вакууме.
Re[2]: Производительность std::bind (GCC 4.7.1)
От: SkyDance Земля  
Дата: 12.09.13 08:09
Оценка:
_>Если тело ф-ции хоть сколько-нибудь осмысленное и вы не гоняете это дело в цикле, то абсолютно нет никакой разницы как вы это все вызываете. А значит подобные замеры не имеют практической ценности.

Разумеется, это гоняется в цикле. Изначально это были strand'ы от boost::asio, и прочие handler'ы, с широким и массовым использованием boost::function & boost::bind.

_>Ну а если все-таки у вас нагружненный, то имеет смысл изучать конкретную ситуацию, а не анализировать абстрактный call в вакууме.


Видимо, мне стоило упомянуть это в начальном сообщении (или даже дать ссылку на предыдущий тред, но я что-то не нашел его), что такой анализ уже проводился, и одним из узких мест как раз стал boost::asio и его handler'ы, и среди них — именно массовые вызовы функций через boost::function и boost::bind.
Три года назад пришлось переписать без использования asio (а также function & bind). Сейчас натолкнулся на похожую ситуацию, и решил перетестировать, изменилось ли что-нибудь за 3 с лишним года — все же C++ 11 стал хорошо поддерживаться в gcc.
Очень многие вещи изменились радикально (вот хотя бы те же per-instance allocators в STL).
Re: Производительность std::bind (GCC 4.7.1)
От: Zhendos  
Дата: 12.09.13 11:55
Оценка: 12 (1) +1
Здравствуйте, SkyDance, Вы писали:


SD>И результаты меня даже слегка удивили.

SD>Во-первых, вызов виртуального метода стабильно быстрее вызова обычного. Во-вторых, начиная с длины строки где-то в размер одной строки кэша (16 символов) разницы с std::bind нет. Да и в целом std::function + std::bind чертовски быстры, даже на пустой строке дают не более 10% замедления.
SD>В общем, я заподозрил, что где-то в чем-то ошибся в тесте. Часы (clock) — HPET, т.е. замеры корректны. Опять же, понимаю, что тут упирается в работу с памятью, но все равно уж больно мал порядок расхождения. Проверял на рвзных машинах, соотношение примерно одинаковое везде.
SD>Может, кто-нибудь мне подскажет, где найти более осмысленные тесты? Интересует сравнение free function vs. virtual function vs. functor vs. std::function + std::bind.

Посмотрел дизасемблер на gcc 4.8.1 + amd64.

Все кроме вызовы кроме bind превратились в вызовы
по статическому адресу, т.е.

callq константа


а вызов с помощью bind превратился в

callq  *%rax


Так что ошибка в недооценке оптимизатора.

Виртуальную функцию стоит вызывать с помощью указателя на базовый класс,
в тестирующую функцию передавать указатель на базовый класс, и иницилизировать
этот указатель с помощью функции находящейся в другой единице трансляции (LTO в этом случае не использовать), или
объявить указатель volatile.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.