Здравствуйте, niXman, Вы писали:
X>о, вспомнил! X>у тебя же буфер фиксированного размера, так?
Да — ради этого всё и затевается.
X>насколько я помню, ты собирался переключать контекст после сериализации каждого мембера?
Нет, только после того как буфер заполнится.
X>а как быть, если какой-то мембер по объему не вмещается в буфер? переключать контекст еще и при заполнении буфера, прям посреди его сериализации?
Контекст планируется переключать только при заполнении буфера.
То есть внутри архива при каждом добавлении (либо считывании — в случае десереализации) будет проверка на заполненность. Как только полностью забили буфер — делаем async_write + yield.
X>ты вот честно скажи, задача реальная или придуманная для разминки?
Весь проект "для разминки", не только эта часть.
Я сначала сделал в лоб — с буфером переменной длинны. И пока ещё не упёрся ни в какие ограничения производительности (точнее нагрузка была небольшой).
Но во-первых это перерасход памяти (так как размер сообщений может быть большим), а во-вторых это лишняя динамическая аллокация.
Раз есть возможность для оптимизации — то наверняка кто-то уже пытался её реализовать (особенно учитывая то, что я сетевыми приложениями никогда не занимался профессионально и сразу наткнулся на эту возможность).
Собственно меня и интересует best practices.
X>и на каком девайсе твой код должен работать?
Хотелось бы на каком-нибудь 2xCPU Haswell'e. Но сначала попробую на чём-то типа Amazon EC2 c3.8xlarge.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>В том то и дело, что на C обычно пишут с использованием runtime полиморфизма — и не парятся, отсюда и все тормоза. EP>Типичный пример — GLib: EP>
Производительность — штука сложная. Подобный код, с указателями на функции вместо шаблонного параметра вполне может работать быстро. Предсказание ветвлений вполне себе работает на указателях на функции, причем даже на древних процессорах. В данном случае, branch predictor будет предсказывать правильный путь со 100% вероятностью, так как для сравнения используется всегда одна и та же функция. Код будет более компактным, следовательно, есть больше шансов что он будет находится в L1 кэше. Впрочем, не думаю, что разницу между временем работы можно будет измерить на практике.
Вообще, главный недостаток вышеприведенного примера кода заключается в том, что он не безопасен, а вовсе не в том, что он медленный.
EP>1. Исключения использовать не обязательно. EP>2. За счёт inline'инга компилятор может увидеть что там нет никаких исключений. EP>3. На x64 используются zero-cost exceptions. Проверка кодов ошибок по всему callpath может быть даже медленней на happy-path чем исключения.
Zero-cost exceptions это как? Если имеются ввиду unwind таблицы, то они добавляют оверхэд по данным и времени обработки исключения. Оверхед по данным часто означает увеличение времени выполнения, из-за кэширования инструкций и данных. Но даже безотносительно самих исключений, С++ код как правило создает кучу не POD объектов, которые должны при создании регистрировать свои деструкторы для последующего вызова во время разворачивания стека. Идиоматичный Си код, который делает то же самое что и идиоматичный С++ код, работает быстрее, так как для освобождения ресурсов он просто выполняет код, ему не нужно проходить по списку деструкторов в стеке и вызывать каждый из них.
EP>В первую очередь "полезного" кода. Плохой код тоже может выполнять полезные функции.
Иногда "хороший" код на С++ это то еще гавно, плавали, знаем
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Отчего же? На современных машинах bandwidth памяти ~30GiB/s — это upper bound для сериализации.
Ну это в крайнем случае, если данные читаются последовательно и пишутся последовательно. На практике, буферы будут в разных, не пересекающихся участках памяти, структуры данных для сериализации — тоже иерархические, а значит в памяти непоследовательно расположены, а значит тоже будут читаться медленно, думаю — пара сотен мегабайт, это верхний предел для такого
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Нет, только после того как буфер заполнится.
а если в буфер помещается только часть мембера, как быть?
EP>Но во-первых это перерасход памяти (так как размер сообщений может быть большим)
перерасход памяти для приложений этого класса, думается мне, это начиная с 256ГБ
EP>а во-вторых это лишняя динамическая аллокация.
заведи пул буферов. не нужно постоянно выделять.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Хотелось бы на каком-нибудь 2xCPU Haswell'e. Но сначала попробую на чём-то типа Amazon EC2 c3.8xlarge.
из своих наблюдений, скажу, что большую часть времени твой сервис наверняка будет находится в режиме ожидания IO. (если это не сервис стримирования видео/аудио, ну или еще что-то что никого не ждет, а просто лупит в сеть пакеты сколько может)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Lazin, Вы писали:
L>Производительность — штука сложная. Подобный код, с указателями на функции вместо шаблонного параметра вполне может работать быстро. Предсказание ветвлений вполне себе работает на указателях на функции, причем даже на древних процессорах. В данном случае, branch predictor будет предсказывать правильный путь со 100% вероятностью, так как для сравнения используется всегда одна и та же функция. Код будет более компактным, следовательно, есть больше шансов что он будет находится в L1 кэше.
Основная плата за косвенный вызов — это отсутствие inline'а и последующего "сращивания" с вызывающим кодом. Заинлайненная функция вполне может скомпилироваться в одну (!) ассемблерную инструкцию, в то время как при косвенном вызове будет полноценная функция со всеми вытекающими.
Более того — часто происходят многоуровневые inline'ы.
L>Впрочем, не думаю, что разницу между временем работы можно будет измерить на практике.
Разница существенная. Если не веришь — можем сделать тест.
L>Вообще, главный недостаток вышеприведенного примера кода заключается в том, что он не безопасен, а вовсе не в том, что он медленный.
Это лирика. Мой основой поинт в том — что дефолтный код на C обычно медленнее аналога на C++.
L>Zero-cost exceptions это как? Если имеются ввиду unwind таблицы,
Да, статические unwind таблицы вместо старых setjmp/longjmp.
L>то они добавляют оверхэд по данным и времени обработки исключения. Оверхед по данным часто означает увеличение времени выполнения, из-за кэширования инструкций и данных.
Отчего же? Эти данные не используются на happy-path -> кэш не засоряют.
L>Но даже безотносительно самих исключений, С++ код как правило создает кучу не POD объектов, которые должны при создании регистрировать свои деструкторы для последующего вызова во время разворачивания стека. Идиоматичный Си код, который делает то же самое что и идиоматичный С++ код, работает быстрее, так как для освобождения ресурсов он просто выполняет код, ему не нужно проходить по списку деструкторов в стеке и вызывать каждый из них.
Если в коде C, нет никаких аналогов деструкторов в виде free и т.п., то почему в варианте C++ деструкторы должны быть не тривиальными?
Здравствуйте, Lazin, Вы писали:
EP>>Отчего же? На современных машинах bandwidth памяти ~30GiB/s — это upper bound для сериализации. L>Ну это в крайнем случае, если данные читаются последовательно и пишутся последовательно.
Это upper bound. Если даже сериализация будет в 20 раз медленнее — то всё равно забьёт сеть
L>На практике, буферы будут в разных, не пересекающихся участках памяти, структуры данных для сериализации — тоже иерархические, а значит в памяти непоследовательно расположены, а значит тоже будут читаться медленно, думаю — пара сотен мегабайт, это верхний предел для такого
Возьмём достаточно экстремальный случай: считали 4байта полезных данных, и нужно прыгать в другой участок памяти за новой порцией. На современном железе считывание из памяти происходит по 64 байта, то есть из считанных 64B полезными оказываются только 4B. Итого: сериализация будет примерно в 16 раз медленней upper bound — но и этого вполне достаточно.
А в реальности же у данных не будет такой сильной ветвистости по памяти.
Здравствуйте, niXman, Вы писали:
EP>>Нет, только после того как буфер заполнится. X>а если в буфер помещается только часть мембера, как быть?
yield. Нет ничего страшного в том, что корутина заснёт на половине поля.
EP>>Но во-первых это перерасход памяти (так как размер сообщений может быть большим) X>перерасход памяти для приложений этого класса, думается мне, это начиная с 256ГБ
Для свободной памяти всегда можно найти применение.
EP>>а во-вторых это лишняя динамическая аллокация. X>заведи пул буферов. не нужно постоянно выделять.
Аллокация буфера переменного размера априори не быстрее, а чаще медленнее чем fixed-size аллокация.
Для fixed-size будет обычный free-list, с нулевой внутренней и внешней фрагментацией, с моментальной аллокацией/деаллокацией.
Для переменного размера одним free-list'ом не отделаться, будет фрагментация, да и соответствующие операции будут дороже.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Возьмём достаточно экстремальный случай: считали 4байта полезных данных, и нужно прыгать в другой участок памяти за новой порцией. На современном железе считывание из памяти происходит по 64 байта, то есть из считанных 64B полезными оказываются только 4B. Итого: сериализация будет примерно в 16 раз медленней upper bound — но и этого вполне достаточно. EP>А в реальности же у данных не будет такой сильной ветвистости по памяти.
Т.е. чаще всего, эти древовидные структуры, которые нужно сериализовать и отправлять, будут небольшими? Если да, то может и не стоит оптимизировать этот path? Пусть оно быстро работает для нормального случая. Можно, например, использовать буфер фикс. размера, в который помещаются целиком нормальные структуры данных, а в случае, если данные не помещаются в буфер фикс. размера — выделять память.
Здравствуйте, Lazin, Вы писали:
EP>>Возьмём достаточно экстремальный случай: считали 4байта полезных данных, и нужно прыгать в другой участок памяти за новой порцией. На современном железе считывание из памяти происходит по 64 байта, то есть из считанных 64B полезными оказываются только 4B. Итого: сериализация будет примерно в 16 раз медленней upper bound — но и этого вполне достаточно. EP>>А в реальности же у данных не будет такой сильной ветвистости по памяти. L>Т.е. чаще всего, эти древовидные структуры, которые нужно сериализовать и отправлять, будут небольшими?
Почему небольшими? Могут быть несколько мебибайт.
Слабая ветвистость по памяти не означает малый размер.
L>Если да, то может и не стоит оптимизировать этот path? Пусть оно быстро работает для нормального случая.
В моём случае асинхронная сериализация пока не критична. Мне больше интересна существующая практика.
L>Можно, например, использовать буфер фикс. размера, в который помещаются целиком нормальные структуры данных, а в случае, если данные не помещаются в буфер фикс. размера — выделять память.
Примерно так и планировал сделать (как один из вариантов) — использовать достаточно просторный fixed-size-buffer, но не слишком большой.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Основная плата за косвенный вызов — это отсутствие inline'а и последующего "сращивания" с вызывающим кодом. Заинлайненная функция вполне может скомпилироваться в одну (!) ассемблерную инструкцию, в то время как при косвенном вызове будет полноценная функция со всеми вытекающими. EP>Более того — часто происходят многоуровневые inline'ы.
Что приводит к раздуванию кода. Компактный код, это всегда хорошо, жаль, что об этом не принято думать в среде любителей современного с++ . К тому же, польза от инлайна на x64 и на x86 с соглашением о вызове __fastcall будет не такой уж и большой, параметры в ф-ю будут переданы через регистры, так как их всего два.
И вообще, это только среди с++ программистов принято надеяться на инлайн и на то, что когда оно схлопнется в одну инструкцию, вот тогда оно будет работать быстро . А я вот, например, буду оптимизировать сортировку путем выбора алгоритма сортировки, максимально подходящего под данные
EP>Разница существенная. Если не веришь — можем сделать тест.
Честно говоря, мне немного лень, особенно сегодня Думаю разница будет, но она будет не драматичной, но заметной. На x86 она будет более заметной, на x64 — менее заметной.
EP>Отчего же? Эти данные не используются на happy-path -> кэш не засоряют.
Исключения же иногда происходят и вымывают happy-path из L1 Это же очень нелокально.
EP>Если в коде C, нет никаких аналогов деструкторов в виде free и т.п., то почему в варианте C++ деструкторы должны быть не тривиальными?
Идиоматичный код на с++ должен их использовать. Без них не написать exception safe код. А вот в Си нет исключений, следовательно, RAII не нужно и можно просто освобождать ресурсы тогда, когда они не нужны.
Здравствуйте, Lazin, Вы писали:
EP>>Основная плата за косвенный вызов — это отсутствие inline'а и последующего "сращивания" с вызывающим кодом. Заинлайненная функция вполне может скомпилироваться в одну (!) ассемблерную инструкцию, в то время как при косвенном вызове будет полноценная функция со всеми вытекающими. EP>>Более того — часто происходят многоуровневые inline'ы. L>Что приводит к раздуванию кода.
It depends.
Если после инлайна остаётся пара простых инструкций — то вполне может быть что в сумме кода будет меньше чем при косвенном вызове.
Но на C++, по крайней мере, есть выбор. Я могу принимать итераторы через шаблоны, а могу через type-erasure, а-ля any_iterator. И оптимизировать под то, что важнее.
L>Компактный код, это всегда хорошо, жаль, что об этом не принято думать в среде любителей современного с++ .
Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3.
Но вообще интересно дискуссия поворачивается:
Со сравнения скорости, с "тот шаблон будет инстанциирован для двух-трех разных типов и его вполне возможно написать на Си несколько раз", перешли на "ну у него хотя бы код компактный"
Скоро возраст как аргумент пойдёт
L>К тому же, польза от инлайна на x64 и на x86 с соглашением о вызове __fastcall будет не такой уж и большой, параметры в ф-ю будут переданы через регистры, так как их всего два.
Кого два? Инлайнятся функции с разным количеством параметров.
L>И вообще, это только среди с++ программистов принято надеяться на инлайн и на то, что когда оно схлопнется в одну инструкцию, вот тогда оно будет работать быстро .
И что характерно — ведь схлопывается
L>А я вот, например, буду оптимизировать сортировку путем выбора алгоритма сортировки, максимально подходящего под данные
Начинается
Конечно же правильный выбор алгоритмов это отличительная особенность C программистов. "Ну и что, что язык медленный — мы вас меньшей сложностью заборим!"
EP>>Отчего же? Эти данные не используются на happy-path -> кэш не засоряют. L>Исключения же иногда происходят и вымывают happy-path из L1 Это же очень нелокально.
Happy-path намного более вероятный — и оптимизировать нужно именно его. Кстати, с кодами ошибок будет засорение всего happy-path runtime проверками.
EP>>Если в коде C, нет никаких аналогов деструкторов в виде free и т.п., то почему в варианте C++ деструкторы должны быть не тривиальными? L>Идиоматичный код на с++ должен их использовать. Без них не написать exception safe код. А вот в Си нет исключений, следовательно, RAII не нужно и можно просто освобождать ресурсы тогда, когда они не нужны.
Может на C и нет exception safety — но ошибки там тоже нужно корректно обрабатывать, чтобы ничего не утекло на return'е.
Никогда не видел "if(errorN) goto ERROR_N;" с длинным хвостом меток в конце функции?
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>yield. Нет ничего страшного в том, что корутина заснёт на половине поля.
звучит страшно %)
EP>Для свободной памяти всегда можно найти применение.
не всегда. даже на оборот.
EP>Аллокация буфера переменного размера априори не быстрее, а чаще медленнее чем fixed-size аллокация.
не нужно постоянно аллоцировать. пул создавай один раз, а дальше просто выбирай и ложи обратно его буфера.
твой сервер же будет принимать соединения от юзеров? — значит для каждого сокета есть сессия. и, т.к. твое дерево общее — в сессии храни итератор, и по хендлеру записи инкрементируй его.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
EP>>yield. Нет ничего страшного в том, что корутина заснёт на половине поля. X>звучит страшно %)
Обычный thread тоже может уснуть где попало. Причём не то что между операциями C++, а даже между ассемблерными инструкциями.
А если ещё рассмотреть weak memory model — то там вообще адЪ
EP>>Аллокация буфера переменного размера априори не быстрее, а чаще медленнее чем fixed-size аллокация. X>не нужно постоянно аллоцировать. пул создавай один раз, а дальше просто выбирай и ложи обратно его буфера.
Ты под аллокцией что понимаешь? Стандартный new/malloc?
Доставать из пула буфер правильного размера, а потом ложить его обратно — это также аллокация/деаллокация.
X>твой сервер же будет принимать соединения от юзеров?
Принимать и инициировать.
X> — значит для каждого сокета есть сессия.
Да, есть.
X>и, т.к. твое дерево общее -
Что значит общее? Одно на сессию? Да — одно.
X>в сессии храни итератор, и по хендлеру записи инкрементируй его.
Так весь вопрос в том, как этот итератор запрограммировать
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>It depends. EP>Если после инлайна остаётся пара простых инструкций — то вполне может быть что в сумме кода будет меньше чем при косвенном вызове. EP>Но на C++, по крайней мере, есть выбор. Я могу принимать итераторы через шаблоны, а могу через type-erasure, а-ля any_iterator. И оптимизировать под то, что важнее.
Это С++, если ф-я инлайнится, то ее тело все равно должно быть в результирующем коде, так как ее может использовать другой модуль.
EP>Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3.
Иногда -Os (и -Oa, за компанию) бывает даже быстрее чем -O3, у меня так было в одном из проектов
EP>Но вообще интересно дискуссия поворачивается: EP>Со сравнения скорости, с "тот шаблон будет инстанциирован для двух-трех разных типов и его вполне возможно написать на Си несколько раз", перешли на "ну у него хотя бы код компактный" EP>Скоро возраст как аргумент пойдёт
Какой еще "возраст как аргумент"? Code bloat это довольно известная проблема С++, разве нет? Template heavy код как правило приводит к огромным бинарникам и соответствующему падению производительности.
L>>К тому же, польза от инлайна на x64 и на x86 с соглашением о вызове __fastcall будет не такой уж и большой, параметры в ф-ю будут переданы через регистры, так как их всего два.
EP>Кого два? Инлайнятся функции с разным количеством параметров.
В случае предиката сортировки, передается всего два параметра. Они легко могут передаваться через регистры процессора, не затрагивая память.
EP>Конечно же правильный выбор алгоритмов это отличительная особенность C программистов. "Ну и что, что язык медленный — мы вас меньшей сложностью заборим!"
Я не троллю, просто типичный обитатель С++ тредов как правило любит рассказывать о том как все круто инлайнится и от этого все быстро работает, а в этом вашем Си quicksort указатель на функцию вообоще дергает, это common sense. Но при этом, наш гипотетический гуру программирования ни разу в жизни не пытался оптимизировать сортировку, выбором алгоритма под конкретные данные, либо рассказывает как круто он сэкономил на вызове заинлайненого предиката, в котором есть cast float-а к int-у, например. Стандартную библиотеку С++ писали лучшие умы человечества, поэтому мы будем использовать ее и только ее везде, чтобы было быстро, а эти непонятные флаги компилятора в студии оставим по дефолту, вдруг что-нибудь сломается
Поэтому я так не люблю все эти разговоры о том, что тот или иной язык медленней работает в сравнении с С++. Это все как правило голословно и озвучивается людьми, которые не могут в оптимизацию. Народ ведь поголовно не знает флаги компилятора, влияющие на оптимизации, кроме O1 O2 O3 Os, не знает сколько стоят разные конструкции, приведения типов там всякие или создание объектов на стеке. Не могу сказать что я прямо такой гуру, BTW
EP>Happy-path намного более вероятный — и оптимизировать нужно именно его. Кстати, с кодами ошибок будет засорение всего happy-path runtime проверками.
Если в коде куча конструкций вроде if (status != SUCCESS) ... и при этом вероятность happy path выше в разы, то branch predictor будет очень точно угадывать ветвления и обработка ошибок практически не будет оказывать влияния на время выполнения.
EP>Может на C и нет exception safety — но ошибки там тоже нужно корректно обрабатывать, чтобы ничего не утекло на return'е. EP>Никогда не видел "if(errorN) goto ERROR_N;" с длинным хвостом меток в конце функции?
Видел, много раз, не фанат
Здравствуйте, Lazin, Вы писали:
EP>>Но на C++, по крайней мере, есть выбор. Я могу принимать итераторы через шаблоны, а могу через type-erasure, а-ля any_iterator. И оптимизировать под то, что важнее. L>Это С++, если ф-я инлайнится, то ее тело все равно должно быть в результирующем коде, так как ее может использовать другой модуль.
Что ты имеешь ввиду? Функция вида:
void foo(any_iterator x);
компилируется один раз, это не шаблон. Её даже можно вынести в динамическую библиотеку.
EP>>Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3. L>Иногда -Os (и -Oa, за компанию) бывает даже быстрее чем -O3, у меня так было в одном из проектов
Иногда такое бывает из-за того, что march/mtune выставлено на GCD (например вместо native), а не из-за размера кода.
L>>>К тому же, польза от инлайна на x64 и на x86 с соглашением о вызове __fastcall будет не такой уж и большой, параметры в ф-ю будут переданы через регистры, так как их всего два. EP>>Кого два? Инлайнятся функции с разным количеством параметров. L>В случае предиката сортировки, передается всего два параметра.
Так мы же про общий случай, сортировка — это просто иллюстрация.
L>Они легко могут передаваться через регистры процессора, не затрагивая память.
Через память, точнее кэш, будет передаваться RSP. Но проблема даже не в этом.
А в том, что теряется возможность сделать оптимизацию заинлайненного кода (зачастую многоуровневую). И плюс дополнительные инструкции.
Более того, если попытаться сделать аналог std::sort для C, тот там будет ещё пара косвенных вызовов из-за:
произвольного типа последовательности
произвольного типа элемента (а не только POD, который можно swap-byte-ить)
EP>>Конечно же правильный выбор алгоритмов это отличительная особенность C программистов. "Ну и что, что язык медленный — мы вас меньшей сложностью заборим!" L>Я не троллю, просто типичный обитатель С++ тредов как правило любит рассказывать о том как все круто инлайнится и от этого все быстро работает, а в этом вашем Си quicksort указатель на функцию вообоще дергает, это common sense.
Так действительно всё круто инлайнится, сам неоднократно убеждался И сортировка это лишь конкретный пример.
L>Но при этом, наш гипотетический гуру программирования ни разу в жизни не пытался оптимизировать сортировку, выбором алгоритма под конкретные данные
Я понятия не имею о каком "гипотетическом гуру" ты говоришь.
Это выглядит как: "вот я знаю сиплюсплюскинка — так он вообще сортировки выбирать не умеет. а вот сишники — другое дело, их медленный qsort буквально подталкивает к правильному выбору алгоритма"
L>Стандартную библиотеку С++ писали лучшие умы человечества, поэтому мы будем использовать ее и только ее везде, чтобы было быстро
Не скажу за всю стандартную библиотеку, но STL действительно писали лучшие умы. По крайней мере это самая лучшая стандартная алгоритмическая библиотека, с самым лучшим интерфейсом и набором алгоритмов.
Тех же сортировок там три штуки: интроспективная (которая сочетает в себе три типа сортировки), пирамидальная, сортировка слиянием (причём адаптивная, а не в лоб).
Плюс алгоритмы связанные с сортировкой: разбиение множества (три штуки — разбиение Энтони Хоара, Нико Ломуто и адаптивное стабильное), слияние (две штуки: out-of-place и адаптивное in-place), поворот (out-of-place и in-place(привет Евклиду)), выбор k-го элемента (причём с полезной перестановкой остальных элементов), бинарный поиск (пять штук) и т.д.
EP>>Happy-path намного более вероятный — и оптимизировать нужно именно его. Кстати, с кодами ошибок будет засорение всего happy-path runtime проверками. L>Если в коде куча конструкций вроде if (status != SUCCESS) ... и при этом вероятность happy path выше в разы, то branch predictor будет очень точно угадывать ветвления и обработка ошибок практически не будет оказывать влияния на время выполнения.
Естественно он будет угадывать, особенно после первого прогона.
Фишка в том, что в случае исключений ему даже угадывать ничего не придётся — cmp+je/whatever не будут разбросаны по всему happy path.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Что ты имеешь ввиду? Функция вида: EP>
EP>void foo(any_iterator x);
EP>
компилируется один раз, это не шаблон. Её даже можно вынести в динамическую библиотеку.
Ты сказал что после инлайна кода может быть меньше. В call site да, может и меньше, но если ф-я заинлайнилась в одном месте, в объектнике все равно должна быть ее полная копия, которую можно вызвать, на которую можно получить указатель. В то время, когда компилятор собирает один модуль, он ничего не знает о том, используется ли эта функция в других. Whole program optimization возможно может это забороть, я не проверял.
EP>>>Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3. L>>Иногда -Os (и -Oa, за компанию) бывает даже быстрее чем -O3, у меня так было в одном из проектов EP>Иногда такое бывает из-за того, что march/mtune выставлено на GCD (например вместо native), а не из-за размера кода.
Ну это вряд ли Os это подмножество O2, без тех оптимизаций, которые могут привести к увеличению размера бинарника.
L>>Они легко могут передаваться через регистры процессора, не затрагивая память. EP>Через память, точнее кэш, будет передаваться RSP. Но проблема даже не в этом. EP>А в том, что теряется возможность сделать оптимизацию заинлайненного кода (зачастую многоуровневую). И плюс дополнительные инструкции. EP>Более того, если попытаться сделать аналог std::sort для C, тот там будет ещё пара косвенных вызовов из-за: EP> произвольного типа последовательности EP> произвольного типа элемента (а не только POD, который можно swap-byte-ить)
Никто не спорит с тем, что заинлайненная версия будет быстрее, просто она не будет сильно быстрее, по описанным мной причинам И таки да, inline swap ф-ии для некоторых типов данных может очень здорово помочь.
EP>Я понятия не имею о каком "гипотетическом гуру" ты говоришь. EP>Это выглядит как: "вот я знаю сиплюсплюскинка — так он вообще сортировки выбирать не умеет. а вот сишники — другое дело, их медленный qsort буквально подталкивает к правильному выбору алгоритма"
Я говорю о распространенной точке зрения в сообществе программистов. Педалим тонны кода, inline вывезет, ведь С++ — самый быстрый Хотя на самом деле все всегда очень контр-интуитивно и очень часто простой код, который, казалось бы, должен работать быстро, работает медленно и стоимость вызовы функций — не всегда самое узкое место в коде. Точку зрения Сишников я тут ни в коем случае представлять не могу.
EP>Не скажу за всю стандартную библиотеку, но STL действительно писали лучшие умы. По крайней мере это самая лучшая стандартная алгоритмическая библиотека, с самым лучшим интерфейсом и набором алгоритмов. EP>Тех же сортировок там три штуки: интроспективная (которая сочетает в себе три типа сортировки), пирамидальная, сортировка слиянием (причём адаптивная, а не в лоб). EP>Плюс алгоритмы связанные с сортировкой: разбиение множества (три штуки — разбиение Энтони Хоара, Нико Ломуто и адаптивное стабильное), слияние (две штуки: out-of-place и адаптивное in-place), поворот (out-of-place и in-place(привет Евклиду)), выбор k-го элемента (причём с полезной перестановкой остальных элементов), бинарный поиск (пять штук) и т.д.
И очень долго там небыло даже банальных хэш таблиц insertion sort мне тоже недавно пришлось писать вручную
EP>>>Happy-path намного более вероятный — и оптимизировать нужно именно его. Кстати, с кодами ошибок будет засорение всего happy-path runtime проверками. L>>Если в коде куча конструкций вроде if (status != SUCCESS) ... и при этом вероятность happy path выше в разы, то branch predictor будет очень точно угадывать ветвления и обработка ошибок практически не будет оказывать влияния на время выполнения.
EP>Естественно он будет угадывать, особенно после первого прогона. EP>Фишка в том, что в случае исключений ему даже угадывать ничего не придётся — cmp+je/whatever не будут разбросаны по всему happy path.
Зато в случае ошибки control flow ломанется в редко используемый, незакэшированый код, что возможно даже приведет к hard page fault
Вообще, о таких вещах невозможно говорить in general. Мой поинт в том, что здесь бывают только конкретные случаи, иногда, коды ошибок будут быстрее, в других случаях, исключения будут быстрее, тоже самое можно сказать обо всех аспектах производительности. Может в каких-то случаях, компилятор разрулит, заинлайнит и оптимизирует и все будет очень быстро, а может быть что-то пойдет не так и zero cost abstraction не получится
Здравствуйте, Lazin, Вы писали:
EP>>Что ты имеешь ввиду? Функция вида: EP>>
EP>>void foo(any_iterator x);
EP>>
компилируется один раз, это не шаблон. Её даже можно вынести в динамическую библиотеку. L>Ты сказал что после инлайна кода может быть меньше. В call site да, может и меньше, но если ф-я заинлайнилась в одном месте, в объектнике все равно должна быть ее полная копия, которую можно вызвать, на которую можно получить указатель.
Тут я имею ввиду другой случай — если нужно избавится от code bloat, или спрятать "шаблонную" функцию в dll — можно использовать type-erasure, а-ля any_iterator, std::function.
EP>>>>Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3. L>>>Иногда -Os (и -Oa, за компанию) бывает даже быстрее чем -O3, у меня так было в одном из проектов EP>>Иногда такое бывает из-за того, что march/mtune выставлено на GCD (например вместо native), а не из-за размера кода. L>Ну это вряд ли Os это подмножество O2, без тех оптимизаций, которые могут привести к увеличению размера бинарника.
Совсем недавно на эту тему на stackoverflow был сравнительный бенчмарк на ряде процессоров.
L>Никто не спорит с тем, что заинлайненная версия будет быстрее, просто она не будет сильно быстрее, по описанным мной причинам
Большая доля вреда от non-inline в том, что он отключает целый класс оптимизаций. Этот вред остаётся даже при отсутствии overhead'а на вызов (не говоря уже про косвенность).
L>И таки да, inline swap ф-ии для некоторых типов данных может очень здорово помочь.
Кстати, qsort до runtime'а даже не знает сколько байтов swap'ить.
EP>>Плюс алгоритмы связанные с сортировкой: разбиение множества (три штуки — разбиение Энтони Хоара, Нико Ломуто и адаптивное стабильное), слияние (две штуки: out-of-place и адаптивное in-place), поворот (out-of-place и in-place(привет Евклиду)), выбор k-го элемента (причём с полезной перестановкой остальных элементов), бинарный поиск (пять штук) и т.д. L>И очень долго там небыло даже банальных хэш таблиц
Это C++11 слишком запоздалый получился.
Де-факто же они были доступны через TR1 или Boost (но это уже не стандарт).
L>insertion sort мне тоже недавно пришлось писать вручную
Я не просто так упомянул std::rotate выше — его как раз можно использовать для реализации insertion sort (хотя лучше std::copy_backward, или даже просто взять готовую и быструю guarded/unguarded insertion sort из SGI STL).
В последних лекциях (17-19) Александра Степанова Efficient Programming with Components
как раз разбирается insertion sort и вариации.
Вот конкретно его слова про insertion sort в STL:
By the way, does STL have insertion sort inside? It has, of course it does.
So, what happend during standartization process: they took something which was in the library and was used by the library and threw it out. The argument was: "oh, we already have too many sorts".
Is it a good argument "too many sorts"? No, you need to have as many sorts as people might need when they do practical things.
прошу простить меня за оффтопик, но решил сообщить, если кто вдруг не в курсе — сегодня начинается ревью boost.fiber. возможно кто-то захочет высказаться/предложить/покритиковать что-нить.
я-то слишком "зелен"(пока?) для этого, но всегда слежу за ревью библиотек предложенных в boost.
спасибо.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP> Внешняя генерация необходимого кода из схемы — а-ля Apache Thrift или Protocol Buffers (кстати, а может там уже есть асинхронная сериализация?).
Использую нечто подобное, но... генераторы типа гугловых выдают практически нативные структуры, соответственно для каждой такой структуры просто явно генерируется код сериализации.
Т.е. рекурсивный обход дерева элементов. Для того чтобы сделать потоковый serialize-deserialize рекурсия не катит, нужно сохранять состояние обхода. Т.е. нужна явная метаинформация с какими то аксессорами.
Тогда состояние обхода можно просто хранить стек class_info+class_object/member_info+member_data. Т.е. не как состояние обхода разнородной структуры, а как состояние обхода однородной структуры мета информации в связке с данными объекта.
Я для подобной задачи сделал генератор который строит не нативные структуры, а что то типа мета-классов, которые рассчитывают самостоятельно layout данных и используют прямой доступ к кускам памяти как к элементам.
Для описания нарисовал свой птичий язык с поддержкой атрибутов. Генератор по файлу строит код инициализации мета-классов, а имея дерево классов позволяющее писать-читать структуры можно уже писать всякие универсальные сериалайзеры-десериалайзеры. Специфика конкретных сериалайзеров определяется через атрибуты в описании.
Т.е. в схеме есть что то типа:
class data
{
[ASN.tag:1] uint32 i;
[ASN.tag:2] string s;
[ASN.tag:3; max:10; composite] string s2;
};
А генерирует оно class_info со списком членов и всей атрибутикой. В class_info рассчитан layout данных и каждый член уже знает свое смещение.
Т.е. имея class_info и какой то абстрактный object можно универсальным образом обойти его структуру и сериализовать (обход дерева + switch по типу на каждой ноде).
Ну и если надо, то состояние обхода несложно запомнить.
А сериалайзеры поверх reflection наклепать просто достаточно, XML/ASN/JSON/CSV/google protobuf и т.д.
А вот для того чтобы можно было как то работать с этим барахлом с сишной стороны генерятся обертки над object с аксессорами get/set которые уже типизированы, но внутре используют reinterpret_cast с нужным смещением. Сериалайзерам эта обертка не нужна, им достаточно class_info + object.
С конструкторами-деструкторами подход немножко не сишный, удобней оказалось работать с nullable членами, в башку пишется ещё и битовая маска представленных элементов. Да хотя там много чего ещё... пространства имён, наследование, юнионы, динамическая загрузка определений (полезно когда часть данных в скриптах обрабатывается) и т.п.
В общем это уже наверное не совсем с++-way, но вариант в котором реализуема потоковая сериализация.