Re[9]: Пример использования шаблонов для оптимизации
От: Evgeny.Panasyuk Россия  
Дата: 24.02.15 15:55
Оценка:
Здравствуйте, chaotic-good, Вы писали:

EP>>Встраивать код из другого translation unit это ещё сложнее, так как требует link-time code generation. Выше был пример фейла оптимизации на constexpr-функции, а если она будет ещё и в другом translation unit — то сфейлится и подавно.

CG>Ну это все сейчас умеют, даже студия

Тем не менее это дополнительный барьер.

EP>>Чем же код в headers сложнее? Чем усложняется процесс разработки?

EP>>Я наоборот предпочёл бы использовать header-only библиотеку, так это многое упрощает — и так будет до тех пор, пока не появятся модули
CG>Ключевое слово "библиотеку" Если смотреть на это как application developer a не как library developer, то получается грусно.

При создании applications естественным образом получаются новые библиотеки. Причём процент того что называется "application code" мал, да и в основном это клей соединяющий библиотеки (свои и third-party).

CG>Вносить изменения в хедеры дороже, так как цикл разработки (edit -> compile -> run tests -> fix errors) замедляется. Изменил одну строчку и ждешь пока пересобирутся десять единиц трансляции, завязанных на этот хедер. У нас в конторе это крайне не приветствуется и в code style есть соотв. раздел про код в хедерах.


1. Precompiled headers.
2. Декомпозируйте заголовки на независимые части. Включайте только то, что требуется.
3. При изменении нешаблонной функции, которая встроена в N мест, LTCG также будет выполнять переборку этих N мест. Хотя возможно масштаб и поменьше чем с заголовками.

EP>>Чем по твоей классификации отличаются простые алгоритмы от сложных? И почему по-твоему простые на шаблонах получается проще, а сложные — нет?


Всё же не понятно почему простые алгоритмы на шаблонах проще, а сложные нет.

EP>>Ты всё же попробуй переведи пример partition_point на динамический полиморфизм, хотя бы схематично.

CG>Я немного во времени ограничен. Уверен что будет хуже чем с шаблонами, придется нагородить класс итератор с виртуальными ф-ями и использвать его. Получится полная хрень a-la java, но скорее всего работать будет не хуже. Если хочешь в code golf поиграть, могу потом как-нибудь наваять.

Полная реализация не нужна, попробуй только интерфейс. Даже в таком простейшем случае без шаблонов повылазят касты типов — так как тип параметра предиката должен быть совместим с value_type итератора, в "чистом ООП" это будет выражено через Object с последующими кастами. В сложных алгоритмах подобных проблем будет больше.
Другой пример это "динамические" мультметоды, которых как известно нет в C++. И если попадётся подобная задача, то их придётся эмулировать, например через visitor, в то время как "статические" мультиметоды элементарно выражаются через перегрузку.

CG>>Ибо в проекте на 400К LOC он всегда использовался с вектором, за исключением одного случая в не критичном к производительности участке кода.

CG>>Как ты наверное понимаешь, иметь подобные методы в хедерах ради перспективы использовать их с std::list или deque без лишнего копирования — очень сомнительна.
EP>>Не понимаю о каком лишнем копировании идёт речь — тут всегда создаётся временная копия.
CG>Ну стандарт не гарантирует использование copy elision тут, так что может быть и лишняя копия, по крайней мере те люди, которые пишут такой код, либо не знают про copy elision, либо не очень на это надеются.

Тут очень простой случай elision, это не какой-нибудь хитрый RVO — компиляторы умеют это делать очень давно. Да и если не будет elision, то это всего лишь move, а не copy.
Да и вообще я не об этом говорил — ты пишешь о какой-то перспективе использования "deque без лишнего копирования", мол там был шаблон из-за того что для deque он что-то улучшит — что ты имеешь ввиду? Для deque там всегда будет копия.

CG>Угу. Вот только это оч. распространенная практика. Очень часто шаблоны появляются в коде там где они не нужны


По моим наблюдениям не "очень часто", а "редко", и в основном это ошибки/перекосы новичков, которые делают ошибки везде, в том числе и в "чистом ООП".

CG>либо параметр функтор — template<class Fn> void apply(Fn const& f); — который можно заменить std::function или чем-нибудь полиморфным.


В общем случае (без soo, и прочего) std::function это дорогая аллокация которую очень непросто соптимизировать компилятору.

EP>>А если много разных аргументов? Причём которые вычисляются как промежуточные значения какого-то алгоритма? Будешь за каждым в матлаб бегать? Генерировать скриптом?

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

Это всё никак не сравнится по удобству, наглядности и краткости с шаблонами (либо constexpr функции в forced context)

CG>Считать факториалы шаблонами IMO самый кривой из вышеперечисленных вариантов.


Почему?
Кстати, мне как раз compile-time факториал пригодился для вычисления объёма симплекса в пространствах различной размерности — проще и лаконичней было сделать metaprogramming hello-world факториал, чем проверять что runtime факториал заинлайнится на семи компиляторах в 14 конфигурациях, или чем вставлять таблицу.

CG>Ну это передергивание уже с GPGPU.


Это всего лишь гротескная иллюстрация к твоему радикальному тезису о том, что компилятор будет делать все оптимизации без помощи программиста, главное чтобы у него была информация в compile-time:

CG>В том то и дело что это не частные случаи. Ты можешь использовать шаблон для генерации более быстрого кода тогда, когда на этапе компиляции есть какая-нибудь информация и ты можешь развернуть какой-нибудь цикл или зафиксировать какой-нибудь параметр или избавиться от косвенного вызова через vtable, но то же самое компилятор делает постоянно и без твоей помощи, когда у него эта информация есть.


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


Пожалуйста, вот gcc 4.9.2 -O3:
#include <functional>
using namespace std;

void foo(function<int(int, int)> x)
{
    volatile auto y = x(111, 222) + 555;
    (void)y;
}

void test_foo()
{
    foo([](int x, int y){ return x+y; });
}
/*********************************************/
template<typename F>
void bar(F x)
{
    volatile auto y = x(111, 222) + 555;
    (void)y;
}

void test_bar()
{
    bar([](int x, int y){ return x+y; });
}

CG>gcc это дело девиртуализирует очень запросто, даже в другой единице трансляции.

Ок, смотрим:
_Z8test_foov:
.LFB1245:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    .cfi_lsda 0x3,.LLSDA1245
    pushq    %rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    movl    $1, %edi
    subq    $48, %rsp
    .cfi_def_cfa_offset 64
    movq    $0, 32(%rsp)
.LEHB0:
    call    _Znwm
.LEHE0:
    leaq    16(%rsp), %rdi
    movl    $222, %edx
    movl    $111, %esi
    movq    %rax, 16(%rsp)
    movq    $_ZNSt17_Function_handlerIFiiiEZ8test_foovEUliiE_E9_M_invokeERKSt9_Any_dataii, 40(%rsp)
    movq    $_ZNSt14_Function_base13_Base_managerIZ8test_foovEUliiE_E10_M_managerERSt9_Any_dataRKS3_St18_Manager_operation, 32(%rsp)
    call    _ZNSt17_Function_handlerIFiiiEZ8test_foovEUliiE_E9_M_invokeERKSt9_Any_dataii
    leaq    16(%rsp), %rsi
    addl    $555, %eax
    movl    $3, %edx
    movl    %eax, 12(%rsp)
    movl    12(%rsp), %eax
    movq    %rsi, %rdi
    call    _ZNSt14_Function_base13_Base_managerIZ8test_foovEUliiE_E10_M_managerERSt9_Any_dataRKS3_St18_Manager_operation
    addq    $48, %rsp
    .cfi_remember_state
    .cfi_def_cfa_offset 16
    popq    %rbx
    .cfi_def_cfa_offset 8
    ret
.L20:
    .cfi_restore_state
    movq    32(%rsp), %rcx
    movq    %rax, %rbx
    testq    %rcx, %rcx
    je    .L19
    leaq    16(%rsp), %rsi
    movl    $3, %edx
    movq    %rsi, %rdi
    call    *%rcx
.L19:
    movq    %rbx, %rdi
.LEHB1:
    call    _Unwind_Resume
.LEHE1:
    .cfi_endproc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
_Z8test_barv:
.LFB1253:
    .cfi_startproc
    movl    $888, -4(%rsp)
    movl    -4(%rsp), %eax
    ret
    .cfi_endproc


Clang частично оптимизирует и встраивает, но только частично и с шаблоном функции никак не сравнится. Хотя пример элементарный — лямбда stateless, применимо SOO.

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


Проблема например в том, что по всему коду размазывается premature pessimization, которая вроде не слишком замедляет, но выкорчевать при необходимости её одним махом не получится, так как нужно перелопачивать весь код.

CG>Я там в той теме отписывался, честно попытался воспроизвести кейс с помощью клэнга 3.6 но ничего не получилось, он генерит здоровую ф-ю, которая проверяет нет ли алиасинга и потом если его нет — использует быстрый векторизованый цикл иначе — более сложную и медленную реализацию. В месте вызова оно вообще инлайнилось и векторизировалось. Я не смог заставить clang сгенирировать плохой код с включенными оптимизациями (-O2)


Clang несомненно радует, но к сожалению не везде. Я часто сравниваю его вывод с GCC — в каких-то местах лучше Clang, а в каких-то GCC. То есть грубо говоря нельзя сказать "мы пишем для Clang'а, поэтому получим все прелести компиляторной оптимизации".
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.