Здравствуйте, niXman, Вы писали:
X>ну да, можно и вовсе на Си писать =)
Тонкие, ответственные участки в проге желательно писать на
так называемом С++ без классов.
Кому как нравится.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Собственно у меня два вопроса: EP>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения? EP>2. Встречалась ли вам такая задача? В каком контексте? И как она была решена?
browser парсит HTML в DOM асинхронно. Т.е. там происходит примерно то что ты описал.
В одном из вариантов имплементации используется push parser ( a.k.a. SAX parser ) c опять же push tokenizer'ом.
Как раз этот вот свой generator я написал для развертки pull parser в push вариант.
(это то для чего ты собираешься coroutines использовать как я понимаю).
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, andrew.f, Вы писали:
AF>>Или Вы серьезно не умеете оптимизировать под разные типы системных вызовов? X>где в моем примере использование системных вызовов? или ты таки считаешь, что printf() это системный вызов?
Хорошо, понял что в этом месте у тебя белое пятно, потому просвещаю.
Следующий код:
В виде машиного кода — это серия call по разным адресам. Замечательно то, что адреса фиксированные, а не грузятся из некой таблицы.
Следующий код:
printf("%s %d %s\n", "Hello", 10, "world");
Это один call, внутри которого пробег по массиву (замечательно оптимизируется загрузкой в кеш процессора) + switch (тоже замечательно оптимизируется.
Сравниваем оба варианта. При увеличении количества параметров std::cout увеличивает количество call. А printf — push в стек. Тут есть какие нибудь непонятки?
По аналогии select vs poll. select — серия системных вызовов. poll — один системный вызов с большим массивом входных данных. Потому poll работает лучше — меньше переключений. Потому printf с кучей параметров работает лучше std::cout — меньше сбрасывания кеша процессора.
Теперь смотрим на твой тест, который и для std::cout и для printf делается тоже самое — вызывает функцию с одним параметром.
НО, в случае std::cout — это call в функцию, в которой не делается ничего лишнего, только конкретный тип параметра преобразуется в строчку.
А в случае printf — это call в функцию, в которой делается switch, чтобы преобразовать конкретный тип параметра в строчку.
Естественно, printf в твоем примере проиграет. Но зная теперь как это устроенно внутри, ты сможешь написать тест, в котором printf таки обгонит std::cout? Или все равно не сможешь?
EP>Вообще-то boost::asio и boost::serialization ортогональны. Более того — они прекрасно работают вместе.
Да это известно. Я о том, что сериализацию можно проводить классами из boost::serialization, а вот для
организации ввода/вывода не прозрачней будет вместо boost::asio использовать, например, в линуксе нативный epoll?
Сама boost::asio, та её часть, что относится к асинхронному вводу/выводу есть обёртка над epoll в линуксе.
Понятно, что потеряется кроссплатформенность, но при использование простого api epoll проще будет реализовать
сложную логику последовательной, растянутой во времени передачи сериализованных данных частями в несколько
тысяч потоков чем эти async_read/async_write которые обязательно поведут себя неизветно как.
EP>Соединений десятки тысяч. Десятки тысяч потоков/процессов не взлетят.
Есть спасение, берём четырёхпроцовый сервак, несколко, с балансировкой между ними.
А как тогда взлетают веб сервисы? Асинхронная обработка в один поток годится только если, собственно,
обработки не много. В противном случае пускаем новый поток/процесс. Связки однопоточный nginx в frontend для распределения запросов
и многопроцессный apache для обработки запросов в backend-стандарт для hayload.
EP>Asio не принципиально — подойдёт любой асинхронный/неблокирующий вывод — и везде будут ровно те же проблемы.
Вот и я о том же, используем более предсказуемые нативные для ОС api kqueue (freebsd), epoll (linux), не знаю
что там есть по этому поводу в win.
Здравствуйте, smeeld, Вы писали:
S>при использование простого api epoll проще будет реализовать сложную логику последовательной, растянутой во времени передачи сериализованных данных частями в несколько тысяч потоков чем эти async_read/async_write которые обязательно поведут себя неизветно как.
S>Есть спасение, берём четырёхпроцовый сервак, несколко, с балансировкой между ними.
S>Вот и я о том же, используем более предсказуемые нативные для ОС api kqueue (freebsd), epoll (linux), не знаю
S>организации ввода/вывода не прозрачней будет вместо boost::asio использовать, например, в линуксе нативный epoll?
вы либо прикалываетесь, либо троллите, либо просто глупы...
S>Сама boost::asio, та её часть, что относится к асинхронному вводу/выводу есть обёртка над epoll в линуксе.
и что? банальный 'operator new' есть обертка. его тоже лучше не юзать?
не представляю, какие вещества нужно употреблять, что писать тут такое %)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Abyx, Вы писали:
A>почему *желательно*, обоснуйте.
Можно задать встречный вопрос?
Вы смотрели на ассемблерные листинги программ на C++ и C?
Если тот кошмар, который генерит компилятор с C++ по сравнению с тем, что получается с C подобного синтаксиса
способствует снижению латентности,
то этот мир не совершенен больше, чем предпологалось.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, Lazin, Вы писали:
L>>Так нельзя сказать в общем случае. Иногда С++ компилятор генерирует менее качественный код, а иногда — более качественный. Идентичный код они вообще будут компилировать одинаково. А еще все очень сильно зависит от приложения и опций компилятора.
EP>Общий случай не интересен. Действительно на C можно сделать код таким же быстрым как и C++. Дело в цене такого кода. EP>Элементарный пример: EP>
EP>template<typename I, typename P>
EP>I find_if(I first, I last, P p)
EP>{
EP> while(first != last)
EP> {
EP> if(p(*first))
EP> return first;
EP> ++first;
EP> }
EP> return first;
EP>}
EP>
такой линейный поиск работает с: EP> любыми типами последовательностей EP> любыми типами элементов EP> любыми предикатами
EP>Попробуй получить такую же гибкость на C не потеряв производительность.
Это уже вопрос культурных различий
Вполне возможно, этот шаблон будет инстанциирован для двух-трех разных типов и его вполне возможно написать на Си несколько раз И вот этот код, вручную специализированый, будет работать быстрее, так как нет исключений, например, и не нужно сохранять данные для разворачивания стека в случае возникновения исключения. Для С++ программиста это звучит странно, но тем не менее, так написано большое количество хорошего кода.
Здравствуйте, 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;" с длинным хвостом меток в конце функции?
Видел, много раз, не фанат
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, Lazin, Вы писали:
EP>>>Но на C++, по крайней мере, есть выбор. Я могу принимать итераторы через шаблоны, а могу через type-erasure, а-ля any_iterator. И оптимизировать под то, что важнее. L>>Это С++, если ф-я инлайнится, то ее тело все равно должно быть в результирующем коде, так как ее может использовать другой модуль.
EP>Что ты имеешь ввиду? Функция вида: EP>
EP>void foo(any_iterator x);
EP>
компилируется один раз, это не шаблон. Её даже можно вынести в динамическую библиотеку.
EP>>>Часто скорость важнее. Я даже не помню когда последний раз видел -Os — везде -O2/-O3. L>>Иногда -Os (и -Oa, за компанию) бывает даже быстрее чем -O3, у меня так было в одном из проектов
EP>Иногда такое бывает из-за того, что march/mtune выставлено на GCD (например вместо native), а не из-за размера кода.
L>>>>К тому же, польза от инлайна на x64 и на x86 с соглашением о вызове __fastcall будет не такой уж и большой, параметры в ф-ю будут переданы через регистры, так как их всего два. EP>>>Кого два? Инлайнятся функции с разным количеством параметров. L>>В случае предиката сортировки, передается всего два параметра.
EP>Так мы же про общий случай, сортировка — это просто иллюстрация.
qsort какой то неудачный пример — функция стандартная, и ей "сто лет в обед", и есть масса других вариантов библиотек сортировки.
В контр-пример можно привести, что printf в большинстве случаев работает быстрее std::cout (можно глянуть на тесты boost::format — там это замечательно видно).
На C можно тоже исходники чистыми хедерниками писать.
И втюхивать в начало каждой функции, как inline, так и static inline, ну и (непотребный конечно вариант, но все таки хоть какая то замена шаблонам из C++) #define.
На выходе получишь те же самые свернутые в одну функцию полезные действия.
И опять же под линуксом Firefox (всеми горячо любимым gcc) официально собирается с -Os, и он правда в такой варианте сборки работает быстрее, чем с -O2/-O3.
L>>Они легко могут передаваться через регистры процессора, не затрагивая память.
EP>Через память, точнее кэш, будет передаваться RSP. Но проблема даже не в этом. EP>А в том, что теряется возможность сделать оптимизацию заинлайненного кода (зачастую многоуровневую). И плюс дополнительные инструкции. EP>Более того, если попытаться сделать аналог std::sort для C, тот там будет ещё пара косвенных вызовов из-за: EP> произвольного типа последовательности EP> произвольного типа элемента (а не только POD, который можно swap-byte-ить)
EP>>>Конечно же правильный выбор алгоритмов это отличительная особенность C программистов. "Ну и что, что язык медленный — мы вас меньшей сложностью заборим!" L>>Я не троллю, просто типичный обитатель С++ тредов как правило любит рассказывать о том как все круто инлайнится и от этого все быстро работает, а в этом вашем Си quicksort указатель на функцию вообоще дергает, это common sense.
EP>Так действительно всё круто инлайнится, сам неоднократно убеждался И сортировка это лишь конкретный пример.
L>>Но при этом, наш гипотетический гуру программирования ни разу в жизни не пытался оптимизировать сортировку, выбором алгоритма под конкретные данные
EP>Я понятия не имею о каком "гипотетическом гуру" ты говоришь. EP>Это выглядит как: "вот я знаю сиплюсплюскинка — так он вообще сортировки выбирать не умеет. а вот сишники — другое дело, их медленный qsort буквально подталкивает к правильному выбору алгоритма"
L>>Стандартную библиотеку С++ писали лучшие умы человечества, поэтому мы будем использовать ее и только ее везде, чтобы было быстро
EP>Не скажу за всю стандартную библиотеку, но STL действительно писали лучшие умы. По крайней мере это самая лучшая стандартная алгоритмическая библиотека, с самым лучшим интерфейсом и набором алгоритмов. EP>Тех же сортировок там три штуки: интроспективная (которая сочетает в себе три типа сортировки), пирамидальная, сортировка слиянием (причём адаптивная, а не в лоб). EP>Плюс алгоритмы связанные с сортировкой: разбиение множества (три штуки — разбиение Энтони Хоара, Нико Ломуто и адаптивное стабильное), слияние (две штуки: 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 будет очень точно угадывать ветвления и обработка ошибок практически не будет оказывать влияния на время выполнения.
EP>Естественно он будет угадывать, особенно после первого прогона. EP>Фишка в том, что в случае исключений ему даже угадывать ничего не придётся — cmp+je/whatever не будут разбросаны по всему happy path.
Какая то подтасовка фактов тут описана.
Проверка на неудачно совершенную операцию в случае с C++ exception все равно есть.
И если операция совершилась неудачно, только в этом случае происходит throw.
Видимо имеется ввиду, когда произошла куча вызовов функций в глубину, и на каждом возврате из вызова функции происходит проверка результата?
Но ведь это замечательно рефакторится (и без всяких goto) разбиением общей функции с ветвлениями на две:
— первая функция последовательные действия с проверкой результата от одиночного действия и в случае неудачи возврат во вторую функцию.
— вторая функция проверяет результат первой и делает то что надо.
static inline
int level1_get_some(int h)
{
int rc;
rc = get_u(h);
if (0 != rc) return rc;
rc = get_v(h);
if (0 != rc) return rc;
rc = get_i(h);
if (0 != rc) return rc;
...
return 0;
}
int level2_get_some(int h)
{
switch(level1_get_some(h))
{
case 0: return 0;
case ....: some_fail_action(...); break;
}
return ...;
}
Заверни rc = action(...); if (0 != rc) в какой нибудь макрос #define do_action(action) ... — получишь компактность. Прикрути static inline и свернется вся эта портянка в одиночный вызов без хитрых лишних переходов cmp+je через каждый call.
В общем все это делается, было бы желание...
Раньше были спецы, про которых говорили: "пишут не на C++, а на C с классами".
Теперь пора пускать в обиход выражение: "пишут на C, как на C++ без классов"
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, andrew.f, Вы писали:
X>ну и зачем ты "это" тут оставил? разве я спрашивал как работает std::cout или printf() ?
Не спрашивал, но судя по коду, приведенного теста, ты просто не в курсе, как оно внутри устроенно, потому и оставил...
X>если ты так и не понял мой посыл, то он состоял только в том — что мне плевать на потроха printf(), и в том, что std::cout имеет больше шансов на успешную оптимизацию и более типобезопасен.
"Типобезопасен" — да, "имеет больше шансов на успешную оптимизацию" — нет.
Либо одно, либо другое...
Надо смотреть на задачу, иногда то небольшое проседание, которое дают STL потоки, бывает критичным.
X>но что я понял, так это то, что ты предлагаешь вместо std::cout юзать *надцать перегрузок рукоблудного printf(). но нет, спасибо, это не мой путь.
Не предлагаю — STL потоки удобнее.
Первоначальный посыл был, что C++-код оптимизируется лучше, в качестве примера был приведен qsort vs std::sort (либо другое по-вкусу).
Только — это пример притянутый за уши...
Здравствуйте, 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, но вариант в котором реализуема потоковая сериализация.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>1. Какие есть ещё альтернативы?
да, после сегодняшнего коммита в YAS, у тебя есть возможность реализовать свои istream и ostream типы так, чтоб они напрямую читали/писали в сокет.
EP>2. Встречалась ли вам такая задача?
да, недавно только закончил с ее реализацией.
уже кратко упоминал о ней:
недели две назад закончил реализацию некоторого механизма зеркалирования и отката состояния группы игровых серверов.
>В каком контексте?
в контексте MMO игровых серверов =)
>И как она была решена?
так, как я описал выше.
(а в YAS я закоммитил это решение позже потому, что YAS все же является форком коммерческого проекта. по этому, этот функционал был сначала реализован в коммерческом прародителе.)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Спасибо.
могу предложить использовать любую поточную сериализацию (YAS, protbuf), подставив декоратор, который нарезает на чанки и асинхронно пишет в сокет_стрим
тут только надо продумать два момента:
1) обработка ошибок при сериализации (часть данных отправили, остальное не отправим)
2) получателю придется все это склеивать и вряд ли получится поточно десериализовать
считаю это наиболее удобным (по сравнению c препроцессором) способом создания RPC/serialization кода для C++
так я делал в коммерческом коде, и так делается в MS idl, google protobuf
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Собственно у меня два вопроса: EP>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения?
JSON
EP>2. Встречалась ли вам такая задача? В каком контексте? И как она была решена?
Я использовал state machime aproach, передавал не сами древовидные структуры, а операции их редактирования. Правда там задача была сложнее — нужно было организовать репликацию большой, древовидной структуры данных на множество машин.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения?
В принципе есть решение, аозволяющее вообще отказаться от буфера. Ведь данные уже и без того есть (дерево), зачем еще один буфер ? Передать бы дерево, да и дело с концом. Понятно, что просто так это невозможно, там указатели, да и лежит оно в памяти бог знает как.
А вот если это дерево и все его внутренности расположены в некотором непрерывном блоке памяти, а вместо указателей используются смещения в этом блоке памяти, то можно перебросить весь этот блок памяти целиком, не копируя ничего и никуда.
Я в свое время нечто подобное делал, для дерева с весьма нетривиальными листьями, содержащими списки и массивы переменной длины. Все это хранилось в буфере, выделенном по механизму memory-mapped files. Для сохранения этого буфера надо было просто его целиком записать в файл , а поскольку буфер был на основе mmf, то и писать ничего не требовалось, просто закрыть mmf. В следующий раз mmf вновь открывался и был сразу готов к употреблению.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Хотелось бы на каком-нибудь 2xCPU Haswell'e. Но сначала попробую на чём-то типа Amazon EC2 c3.8xlarge.
из своих наблюдений, скажу, что большую часть времени твой сервис наверняка будет находится в режиме ожидания IO. (если это не сервис стримирования видео/аудио, ну или еще что-то что никого не ждет, а просто лупит в сеть пакеты сколько может)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
прошу простить меня за оффтопик, но решил сообщить, если кто вдруг не в курсе — сегодня начинается ревью boost.fiber. возможно кто-то захочет высказаться/предложить/покритиковать что-нить.
я-то слишком "зелен"(пока?) для этого, но всегда слежу за ревью библиотек предложенных в boost.
спасибо.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
AF>В контр-пример можно привести, что printf в большинстве случаев работает быстрее std::cout (можно глянуть на тесты boost::format — там это замечательно видно).
что за бред?!
с каких таких пор эта сишная всезаглатывающая кишка стала быстрее плюсовых потоков?!
#include <cstdio>
#include <iostream>
#include <chrono>
enum io_type { test_printf, test_stdio };
template<io_type>
struct test;
template<>
struct test<test_printf> {
template<typename T>
static std::chrono::milliseconds
run(const T *arr, std::size_t size) {
auto start = std::chrono::system_clock::now();
for ( ; size; --size ) {
printf("%d", *arr++);
}
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()-start);
}
};
template<>
struct test<test_stdio> {
template<typename T>
static std::chrono::milliseconds
run(const T *arr, std::size_t size) {
auto start = std::chrono::system_clock::now();
for ( ; size; --size ) {
std::cout << *arr++;
}
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now()-start);
}
};
int main() {
enum { size = 1024*1024*4 };
int *ia = new int[size];
auto t1 = test<test_printf>::run(ia, size);
std::cerr << "printf time =" << t1.count() << std::endl;
auto t2 = test<test_stdio >::run(ia, size);
std::cerr << "stdio time =" << t2.count() << std::endl;
}
запускаем:
./iospeed 1>/dev/null
получаем:
printf time =323
stdio time =183
да и как вообще может быть такое, чтоб printf() который вообще ничего не знает о типе, выполнялся быстрее типизированного кода?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>А конечный автомат выписывался руками или была какая-то автоматизация?
Я имел ввиду подход https://en.wikipedia.org/wiki/State_machine_replication
У нас в архитектуре был большой лог событий, через который выполнялись все модификации данных (можно назвать это event sourcing). Если нужно передавать не разные версии одних и тех же структур данных, то это не очень подходит. Зависит от задачи. Часто это работает.
Здравствуйте, Lazin, Вы писали:
L>Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>>Собственно у меня два вопроса: EP>>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения? L>JSON
в JSON нет типизации, поэтому для де-сериализации полиморфных типов нужен промежуточный этап парсинга в DOM,
тогда как форматы поддерживающие типизацию (например yaml) позволяют использовать потоковый парсер (типа как SAX)
Здравствуйте, niXman, Вы писали:
X>собственно говоря, корутины для этого не нужны. если ты предполагаешь отправлять в сокет каждый такой элемент, то следующий ты можешь брать в хендлере операции асинхронной записи.
fixed X>но тут мне не понятно, как быть в случае, когда ты сериализуешь свое дерево, а во время охидяния хендлера происходит изменение этого дерева? или на момент когда ты начинаешь сериализовать это дерево, оно не будет изменяться до тех пор, пока сериализация не завершится? но даже в этом случае я не очень понимаю, для чего корутины
Последовательная асинхронная сериализация, такты которой запускаются хендлерами из async_write(in_blum.. , с оглядкой на счётчики и флаги,
которые часть специализированного класса, и которые отрабатывают в отдельном потоке или даже процессе, запускаемом из хендлера async_accept?
Тут можно интересный конечный автомат наваять
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Я же вообще не предлагал сериализовать. Храни как хочешь, только в непрерывном куске. Хочешь — храни в нем нормальный int64[], хочешь — массив байтов некоего размера с упакованными int64, а за ним дополнительные вспомогательные структуры. Что хочешь, то и храни. А потом отправь этот блок памяти куда следует целиком.
В данном случае формат специфицирован, и там именно сериализация.
А так — да, иногда можно обойтись и zero-copy.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Но смотрю я на этот вариант и мне грустно от того, что происходит перерасход памяти Эти структуры могут занимать от пары кибибайт до нескольких мебибайт, а соединений — десятки тысяч.
еще не очень ясно как часто изменяются данные и насколько разные данные рассылаются клиентам
в нашем приложении есть задача раздавать одинаковые данные разным клиентам (только сериализованные данные немного отличаются, т.к. содержат инфу о клиенте) и мы эту задачу планируем отдать отдельному сервису, который в сериализованном сообщениии просто будет перезатирать инфу о клиенте. то есть некий броудкастер. у сервера с броудкастером одно соединение, у броудкастера много соединений с внешними клиентами
есть еще такой вариант: если разным клиентам раздаются одинаковые данные, то проще их закешировать результат сериализации) и не делать часто одну и ту же сериализацию
Здравствуйте, smeeld, Вы писали:
X>>ну да, можно и вовсе на Си писать =) S>Тонкие, ответственные участки в проге желательно писать на S>так называемом С++ без классов.
почему *желательно*, обоснуйте.
S>Кому как нравится.
вопрос не в личных пристрастиях, а в том как получится более безопасный более поддерживаемый код.
Здравствуйте, smeeld, Вы писали:
S>Вы смотрели на ассемблерные листинги программ на C++ и C?
я — смотрел. и да, С++ компилятор генерит более качественный код, и оптимизирует лучше.
а что я, по вашему, должен был там еще увидеть?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, 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>В первую очередь "полезного" кода. Плохой код тоже может выполнять полезные функции.
Иногда "хороший" код на С++ это то еще гавно, плавали, знаем
Здравствуйте, 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>>Основная плата за косвенный вызов — это отсутствие 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;" с длинным хвостом меток в конце функции?
Здравствуйте, smeeld, Вы писали:
S>И это компилируется в компактный простой ассемблерный листинг с jmp/jne/je.. скачками в пределах файла.
Этот вариант даже медленнее чем вариант с goto.
S>В случае же использования С++ try/catch сплошные call XXXXXXX S> в дебри либ и фреймворков, которые вызываются по ходу выполнения потока, постоянно прерывая его.
L>if-ы и switch для редко выполняющихся условий, это практически zero overhead. Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch. Т.е. код, выбирающий обработчик для кода ошибки у компилятора получится более быстрым, нежели код для try catch.. catch.. catch, причем намного. Я уже молчу про локальность этого кода
т.е. ты хочешь сказать, что обработка исключительных ситуаций это штатное выполнение программы?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, andrew.f, Вы писали:
AF>>По аналогии select vs poll. select — серия системных вызовов. poll — один системный вызов с большим массивом входных данных. X>т.е. в твоей реальности, select() мониторит только один дескриптор? (страшно жить) X>и да, плюс poll() не в этом.
Что то аж лень писать, как и тебе впрочем.... Каждый о своем, а в результате спор ни о чем....
Нет не на один — select завязан на значение FD_SETSIZE.
И да я в курсе, что select иногда работает быстрее epoll...
И синхронное API скорострельнее ассинхронного ....
Ну и про остальное тоже ....
И вообще фигня все это — адекватный выбор алгоритмов дает основную оптимизацию, а не тонкие технические выкрутасы...
Допустим у нас есть stream socket (например boost::asio::ip::tcp::socket) с асинхронными операциями чтения/записи, а-ля async_read/async_write.
По этому потоку нужно гонять туда-сюда древовидные структуры, то есть обыкновенная сериализация.
Эти структуры не trivially copyable (как минимум потому, что содержат массивы переменной длины) — следовательно zero-copy никак не получится.
Простейшим вариантом для сериализации/десериализации является работа через промежуточный буфер в который помещаются сырые байты всей структуры целиком (в поток записывается размер всей структуры, поэтому при чтении можно аллоцировать буфер подходящего размера).
Но смотрю я на этот вариант и мне грустно от того, что происходит перерасход памяти Эти структуры могут занимать от пары кибибайт до нескольких мебибайт, а соединений — десятки тысяч.
Возможно несколько уменьшить масштаб проблемы распилив крупные корневые структуры на несколько маленьких.
Более экономичным был бы вариант с использованием небольшого буфера фиксированного размера, например 32KiB — и соответственно поэтапная сериализация. То есть прочитали один chunk и сразу его десериализовали (и vice versa для сериализации).
Также это избавило бы от необходимости искать место для буфера переменного размера.
Варианты которые позволили бы использовать фиксированный буфер:
Отдельный поток для сериализации. Это, естественно, совсем не интересно.
Stackful Coroutines — на мой взгляд самый лучший вариант. Да и к тому же они есть не просто в Boost, а даже прям в Boost.Asio.
Описание схемы сериализации в специальном формате, например через expression templates (а-ля Boost.Proto), через гетерогенные последовательности (а-ля Boost.Fusion, тем более сейчас через него всё и сериализуется), либо через препроцессор (BOOST_PP_SEQ или X-Macro) — и последующий распил считывания кусочков данных на continuations.
Внешняя генерация необходимого кода из схемы — а-ля Apache Thrift или Protocol Buffers (кстати, а может там уже есть асинхронная сериализация?).
Собственно у меня два вопроса:
1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения?
2. Встречалась ли вам такая задача? В каком контексте? И как она была решена?
собственно говоря, корутины для этого не нужны. если ты предполагаешь отправлять в сокет каждый такой элемент, то следующий ты можешь брать в хендлере операции асинхронной записи.
но тут мне не понятно, как быть в случае, когда ты сериализуешь свое дерево, а во время охидяния хендлера происходит изменение этого дерева? или на момент когда ты начинаешь сериализовать это дерево, оно не будет изменяться до тех пор, пока сериализация не завершится? но даже в этом случае я не очень понимаю, для чего корутины
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
EP>Простейшим вариантом для сериализации/десериализации является работа через промежуточный буфер в который помещаются сырые байты всей структуры целиком (в поток записывается размер всей структуры, поэтому при чтении можно аллоцировать буфер подходящего размера). EP>Но смотрю я на этот вариант и мне грустно от того, что происходит перерасход памяти Эти структуры могут занимать от пары кибибайт до нескольких мебибайт, а соединений — десятки тысяч.
Пока шёл на тренировку пришёл в голову ещё один вариант:
Использовать deque как буфер, а не vector. В этом случае chunk'и можно деаллоцировать по мере десериализации. Это немного уменьшает peak memory usage, но всё равно не супер айс.
Плюс, чанки от разных дэк можно хранить в одном freelist'е — аллокация/деаллокация будет копеечной.
Плюс, можно сделать оптимизацию в виде zero-copy в некоторых местах — если в какой-то момент у нас идут POD'ы, то чанки можно splice'ить прямо в target дэку.
Здравствуйте, niXman, Вы писали:
EP>>1. Какие есть ещё альтернативы? X>да, после сегодняшнего коммита в YAS, у тебя есть возможность реализовать свои istream и ostream типы так, чтоб они напрямую читали/писали в сокет.
Асинхронно буферами фиксированного размера?
EP>>2. Встречалась ли вам такая задача? X>да, недавно только закончил с ее реализацией. X>уже кратко упоминал о ней: X>
X>недели две назад закончил реализацию некоторого механизма зеркалирования и отката состояния группы игровых серверов.
Да, я видел — интересно. Это для fault tolerance? (никогда профессионально не занимался сетью)
Здравствуйте, niXman, Вы писали:
X>я, кажется понял, что ты имеешь ввиду упоминая корутины... X>типа этого: X>
X>container type data;
X>oarchive oa;
X>for ( const auto &it: data ) {
X> oa & it;
X> yield();
X>}
X>
X>?
Да, примерно. yield можно спрятать в архив — пользовательский код и не будет знать о том, что его прерывают.
То есть пользователь будет просто видеть/писать сериализацию в стиле Boost.Serialization-like.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Асинхронно буферами фиксированного размера?
ну во-первых — это как тебе угодно. все что тебе потребуется, это реализовать свой ostream класс, с единственным необходимым YAS`у методом: 'std::size_t write(const void *ptr, const std::size_t size)'.
во-вторых — чтоб говорить о буферах/преаллокациях/пуле_буфферов — нужно знать скорость поступления данных, и скорость их выгрузки. это и определит стратегию.
EP>Это для fault tolerance?
да.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
X>собственно говоря, корутины для этого не нужны. если ты предполагаешь отправлять в сокет каждый такой элемент, то следующий ты можешь брать в хендлере операции асинхронной записи.
Собственно это то, как оно будет реализовано на нижнем уровне — скидываем один буфер, в хэндлере async_write — запись следующего и так далее.
Вопрос в том, как это сделать в удобном виде, а не в рукопашку раскладывать сериализацию каждой структуры на хэндлеры-continutaions.
Например, у нас есть:
template<typename Archive>
void serialize(Archive &ar, Widget &x)
{
ar & x.a;
ar & x.b;
ar & x.c;
ar & x.d;
}
плюс есть буфер фиксированного размера:
array<char, 1024> fixed_buffer;
этот буфер будет передаваться по сети через async_read/async_write.
Весь Widget больше чем буфер, и за один присест его не получится отправить/получить через fixed_buffer.
Задача сделать сериализацию по кускам, но не меняя код шаблона функции serialize. На stackful coroutines это делается элементарно.
Меня же интересует — существуют ли какие-нибудь другие удобные способы?
X>но тут мне не понятно, как быть в случае, когда ты сериализуешь свое дерево, а во время охидяния хендлера происходит изменение этого дерева? или на момент когда ты начинаешь сериализовать это дерево, оно не будет изменяться до тех пор, пока сериализация не завершится?
Нет, изменение дерева не происходит. Точнее если потребуется, то при записи можно освобождать уже записанные части (делать shrink_to_fit для уже записанных массивов и т.п.)
Здравствуйте, niXman, Вы писали:
EP>>Асинхронно буферами фиксированного размера? X>ну во-первых — это как тебе угодно. все что тебе потребуется, это реализовать свой ostream класс, с единственным необходимым YAS`у методом: 'std::size_t write(const void *ptr, const std::size_t size)'.
В этом конкретном случае — формат структур уже специфицирован, причём он довольно простой. Никаких особых фич как в Boost.Serialization — например отслеживание указателей, сериализация полиморфных типов — не требуется.
Сейчас всё ядро сериализации — это in/out archive (которые пришлось бы писать в любом случае) + рекурсивная сериализация Boost.Fusion Forward Sequence. На всё про всё — меньше ста строк.
Структуры определяются просто как BOOST_FUSION_DEFINE_STRUCT и уже готовы к сериализации.
Подменив архив на тот, который вызывает async_write/async_read, и передаёт yield как хэнделер — как раз и получится асинхронная сериализация. Вопрос же в том — есть ли другие способы.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Собственно это то, как оно будет реализовано на нижнем уровне — скидываем один буфер, в хэндлере async_write — запись следующего и так далее. EP>Вопрос в том, как это сделать в удобном виде, а не в рукопашку раскладывать сериализацию каждой структуры на хэндлеры-continutaions.
как вариант — написать обертку над yas::*_oarchive:
EP>Меня же интересует — существуют ли какие-нибудь другие удобные способы?
мне такие не известны. если найдешь — расскажи плиз.
EP>Нет, изменение дерева не происходит.
так если у тебя неизменяемый объект — то что мешает таки реализовать свой ostream класс, и в нем, когда его внутренний буфер достигает предельного размера — отсылать собранные данные, и снова заполнять буфер следующими данными?
чтоб не было бауз в использовании сети — завести два буфера: первый отдаешь на отправку, второй — для сериализации в него. в хендлере отправки данных переключаешь буфера — второй на отправку, а первый для сериализации.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, uzhas, Вы писали:
EP>>Спасибо. U>могу предложить использовать любую поточную сериализацию (YAS, protbuf), подставив декоратор, который нарезает на чанки
Нарезать на чанки как раз не проблема. Как только заполнился буфер — сразу пишем.
U>и асинхронно пишет в сокет_стрим
Основной фокус как раз тут.
Вот вызвали async_write для первого чанка. Следующий async_write должен происходить из хэндлера записи.
А пока пишется — мы просто останавливаем сериализацию и отдаём управление io_service'у (thread-то всего один). Но — нам нужно не потерять callstack чтобы продолжить запись с правильной позиции в структуре.
Один из вариантов решения — это stackful coroutines, которые как раз сохраняют наш callstack до лучших времён вызова хэндлера.
Здравствуйте, Evgeny.Panasyuk, Вы
EP>Основной фокус как раз тут. EP>Вот вызвали async_write для первого чанка. Следующий async_write должен происходить из хэндлера записи.
Почему должен? Я предлагаю делать полную сериализацию в одном потоке, а запись в сокет делать чанками асинхронно: async_write всегда вызывается из потока сериализации
чего-то подобного, там даже ручной демультиплексор для наглядности).
EP>>Меня же интересует — существуют ли какие-нибудь другие удобные способы? X>мне такие не известны. если найдешь — расскажи плиз.
Ок, хорошо.
EP>>Нет, изменение дерева не происходит. X>так если у тебя неизменяемый объект — то что мешает таки реализовать свой ostream класс, и в нем, когда его внутренний буфер достигает предельного размера — отсылать собранные данные, и снова заполнять буфер следующими данными? X>чтоб не было бауз в использовании сети — завести два буфера: первый отдаешь на отправку, второй — для сериализации в него. в хендлере отправки данных переключаешь буфера — второй на отправку, а первый для сериализации.
А как отдать управление io_service'у после первой отсылки? Ок, допустим мы как-то решили эту задачу. Но сериализация в большинстве случаев будет на порядки быстрее отправки/получения чанков.
Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали.
Здравствуйте, uzhas, Вы писали:
EP>>Основной фокус как раз тут. EP>>Вот вызвали async_write для первого чанка. Следующий async_write должен происходить из хэндлера записи.
U>Почему должен?
Во-первых, порядок двух асинхронных операций не определён. Во-вторых, async_write не атомарен, и сам состоит из нескольких операций типа async_write_some, которые могут перемешиваться с async_write_some от другого async_write.
Но даже если у нас есть строгий порядок асинхронных записей (допустим у нас есть очередь (переменного размера, что уже не хорошо)) — то всё равно не понятно как это будет работать:
В момент вызова каждого async_write у нас уже должен быть готов соответствующий буфер, чего мы изначально и пытались избежать.
Или для наглядности рассмотрим чтение — async_read. Сразу после запуска первого чтения мы не можем запустить второе, так как мы ещё первый кусок данных не распрасили.
U>Я предлагаю делать полную сериализацию в одном потоке, а запись в сокет делать чанками асинхронно: async_write всегда вызывается из потока сериализации
Поток в смысле отдельный thread для сериализации, который блокируется на записи/чтении? То есть вариант #1 из первого сообщения?
Это не подходит — у нас по условию задачи десятки тысяч соединений, плюс оверхед на межпоточную синхронизацию совсем не нужен.
Здравствуйте, antropolog, Вы писали:
EP>>Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали. A>именно, поэтому может стоит рассмотреть как альтернативу модель реактора вместо проактора? A>http://www.boost.org/doc/libs/1_55_0b1/doc/html/boost_asio/overview/core/reactor.html
Непонятно чем это лучше проактора для данной задачи.
По сути точно такая же неблокирующая операция, которая выполнится в будущем — нам точно также нужно сохранять текущий callstack и отдавать управление.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Непонятно чем это лучше проактора для данной задачи.
тем что не надо заранее аллоцировать буфер
EP>По сути точно такая же неблокирующая операция, которая выполнится в будущем — нам точно также нужно сохранять текущий callstack и отдавать управление.
нет, это реактор, и операция записи/чтения блокирующая, только она выполняется моментально т.к. в сокет уже готов для чтения/записи ( коллбек зовётся при появлении возможности писать/читать )
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>А как отдать управление io_service'у после первой отсылки?
эм..
EP>Но сериализация в большинстве случаев будет на порядки быстрее отправки/получения чанков.
ох и не факт =)
обновил информацию о сравнительном тесте.
EP>Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали.
нужен итерационный алгоритм, с возможностью сохранения значения итератора. но ты говоришь, что тебе нужна и возможность итерировать члены класса...
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>А как отдать управление io_service'у после первой отсылки?
эм..
EP>Но сериализация в большинстве случаев будет на порядки быстрее отправки/получения чанков.
ох и не факт =)
обновил информацию о сравнительном тесте.
EP>Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали.
нужен итерационный алгоритм, с возможностью сохранения значения итератора. но ты говоришь, что тебе нужна и возможность итерировать члены класса...
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Lazin, Вы писали:
L>JSON
хм..
вот у меня есть архив бинарного представления, и архив JSON представления — оба содержат абсолютно одни и те же данные. чем json лучше?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, antropolog, Вы писали:
EP>>Непонятно чем это лучше проактора для данной задачи. A>тем что не надо заранее аллоцировать буфер
Это понятно.
EP>>По сути точно такая же неблокирующая операция, которая выполнится в будущем — нам точно также нужно сохранять текущий callstack и отдавать управление. A>нет, это реактор, и операция записи/чтения блокирующая, только она выполняется моментально т.к. в сокет уже готов для чтения/записи ( коллбек зовётся при появлении возможности писать/читать )
Да, но готов-то он будет потом, а не сейчас. А пока он не готов — нужно отдать управление другим, при этом не потеряв свой стэк.
Здравствуйте, Lazin, Вы писали:
EP>>Собственно у меня два вопроса: EP>>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения? L>JSON
Ты про готовые XML/JSON SAX?
Да, я вспоминал это — но в данном конкретном случае спецификация формата уже есть, и он бинарный.
EP>>2. Встречалась ли вам такая задача? В каком контексте? И как она была решена? L>Я использовал state machime aproach, передавал не сами древовидные структуры, а операции их редактирования. Правда там задача была сложнее — нужно было организовать репликацию большой, древовидной структуры данных на множество машин.
А конечный автомат выписывался руками или была какая-то автоматизация?
Здравствуйте, niXman, Вы писали:
EP>>Но сериализация в большинстве случаев будет на порядки быстрее отправки/получения чанков. X>ох и не факт =) X>обновил информацию о сравнительном тесте.
Но всё же — у памяти пропускная способность десятки гигабайт в секунду. У сети же на порядок, а то и на два меньше.
Это надо как-то сильно постараться чтобы бинарная сериализация была медленней I/O.
EP>>Отправив первый чанк — мы заполним второй быстрее чем сообщение будет отправлено, и теперь у нас уже два неотправленных буфера. То есть мы вернулись к тому, с чего начали. X>нужен итерационный алгоритм, с возможностью сохранения значения итератора. но ты говоришь, что тебе нужна и возможность итерировать члены класса...
Да, именно так.
Один из вариантов это Boost.Fusion — он как раз позволяет итерировать члены класса (он и сейчас у меня итерирует, но в контексте синхронной сериализации).
Основное неудобство (использования Fusion для асинхронной сериализации, вместо корутин) в том, что для базовых типов, или каких-то особенных пользовательских (например массивы), придётся выписывать конечный автомат вручную.
Здравствуйте, c-smile, Вы писали:
CS>browser парсит HTML в DOM асинхронно. Т.е. там происходит примерно то что ты описал. CS>В одном из вариантов имплементации используется push parser ( a.k.a. SAX parser ) c опять же push tokenizer'ом.
Да, SAX это как раз пример подобного. Но у меня бинарная структура, со специфицированном форматом.
Apache Thrift / Protocol Buffers могли бы тоже генерировать необходимые конечные автоматы (но вроде ещё не умеют?).
CS>Как раз этот вот свой generator я написал для развертки pull parser в push вариант. CS>(это то для чего ты собираешься coroutines использовать как я понимаю).
Да — у тебя фактически stackless coroutine (кстати, подобное есть в Boost). Из-за того что она stackless — их понадобится несколько выстроенных в цепочку (по корутине на каждый узел дерева).
Причём каждый уровень должен будет знать об использовании корутин. То есть сделать полностью прозрачно для промежуточных уровней, как при использовании stackful coroutine, не получится.
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, Lazin, Вы писали:
L>>JSON X>хм.. X>вот у меня есть архив бинарного представления, и архив JSON представления — оба содержат абсолютно одни и те же данные. чем json лучше?
На самом деле, JSON почти всем лучше Проще перечислить недостатки: не самое компактное представление, особенно для бинарных данных и не самая оптимальная скорость кодирования/декодирования. В остальном — одни достоинства, недаром весь интернет использует JSON и ему подобные представления, вместо бинарных архивов.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Во-первых, порядок двух асинхронных операций не определён. Во-вторых, async_write не атомарен, и сам состоит из нескольких операций типа async_write_some, которые могут перемешиваться с async_write_some от другого async_write.
ну это явно недостатки сетевой библиотеки, которую ты используешь. неатомарность async_write — это идеологический глюк.
EP>В момент вызова каждого async_write у нас уже должен быть готов соответствующий буфер, чего мы изначально и пытались избежать.
мы пытались избежать больших буферов, чтобы сэкономить на памяти. если у нас все чанками передается, то достаточно иметь некий пул небольших буферов
EP>Или для наглядности рассмотрим чтение — async_read. Сразу после запуска первого чтения мы не можем запустить второе, так как мы ещё первый кусок данных не распрасили.
можем, я не вижу архитектурных ограничений. опять же я не знаю, является ли десериализация потоковой. к примеру, парсинг FIX сообщений вполне себе потоковый и мы этим пользуемся, когда парсим сообщение внутри метода on_data (см. ниже)
EP>Поток в смысле отдельный thread для сериализации, который блокируется на записи/чтении?
нет, я плохо выразился. хотел подчеркнуть, что мы не делаем сериализацию одного сообщения то в одном, то во втором потоке. это вполне можно сделать в одном потоке (для разных сообщений это могут быт разные потоки)
основная проблема в том, что у тебя в голове ограничение в виде буста, а я с ним не знаком, поэтому плохо понимаем друг друга. я работаю с сетевой библиотекой, которая имеет внутри нужный пул буферов и которая не имеет read_async, зато имеет on_data(void*, size), то есть модель доставки данных другая. при этом on_data вызывается из IO completion потоков с гарантией последовательности вызова on_data
для разнообразия все же рекомендую отвлечься от буста и посмотреть как устроены асинхронные API (файлы, сеть) в винде и линуксе, быть может, там найдется более удобная модель для данной задачи
Здравствуйте, uzhas, Вы писали:
EP>>В момент вызова каждого async_write у нас уже должен быть готов соответствующий буфер, чего мы изначально и пытались избежать. U>мы пытались избежать больших буферов, чтобы сэкономить на памяти. если у нас все чанками передается, то достаточно иметь некий пул небольших буферов
Сериализация быстрее I/O — что делать когда все буферы заполнены, но ещё не отправлены? (суммарный размер буферов меньше самой структуры — мы же экономим)
EP>>Или для наглядности рассмотрим чтение — async_read. Сразу после запуска первого чтения мы не можем запустить второе, так как мы ещё первый кусок данных не распрасили. U>можем, я не вижу архитектурных ограничений. опять же я не знаю, является ли десериализация потоковой. к примеру, парсинг FIX сообщений вполне себе потоковый и мы этим пользуемся, когда парсим сообщение внутри метода on_data (см. ниже)
В FIX'е какой максимальный/типичный размер сообщения?
U>основная проблема в том, что у тебя в голове ограничение в виде буста, а я с ним не знаком, поэтому плохо понимаем друг друга. я работаю с сетевой библиотекой, которая имеет внутри нужный пул буферов и которая не имеет read_async, зато имеет on_data(void*, size), то есть модель доставки данных другая. при этом on_data вызывается из IO completion потоков с гарантией последовательности вызова on_data U>для разнообразия все же рекомендую отвлечься от буста и посмотреть как устроены асинхронные API (файлы, сеть) в винде и линуксе, быть может, там найдется более удобная модель для данной задачи
Мне кажется другое асинхронное API никак не поможет, в принципе.
Вот даже в твоём случае — с on_data. Для десериализации структуры должно быть вызвано несколько on_data, так как буфер фиксированный, а структура в него не помещается.
Как процедура сериализации уйдёт в сон пока ожидает следующую on_data? Как продолжит десереализацию в правильном состоянии когда придут данные?
Здравствуйте, Pavel Dvorkin, Вы писали:
EP>>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения? PD>В принципе есть решение, аозволяющее вообще отказаться от буфера. Ведь данные уже и без того есть (дерево), зачем еще один буфер ? Передать бы дерево, да и дело с концом. Понятно, что просто так это невозможно, там указатели, да и лежит оно в памяти бог знает как. PD>А вот если это дерево и все его внутренности расположены в некотором непрерывном блоке памяти, а вместо указателей используются смещения в этом блоке памяти, то можно перебросить весь этот блок памяти целиком, не копируя ничего и никуда.
Как я уже говорил эти структуры не trivially copyable. А так да — было бы проще всего (и эффективней) сделать zero-copy.
EP>Как я уже говорил эти структуры не trivially copyable. А так да — было бы проще всего (и эффективней) сделать zero-copy.
Ты писал
>Эти структуры не trivially copyable (как минимум потому, что содержат массивы переменной длины) — следовательно zero-copy никак не получится.
Наличие массивов переменной длины помешать не может. Если есть другие, более серьезные причины — тогда да.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Наличие массивов переменной длины помешать не может. Если есть другие, более серьезные причины — тогда да.
В принципе да, но также есть и целые числа переменной длины — чтобы знать где что находится, нужно как минимум распарсить такие числа и т.п.
Меня больше интересует общий механизм, со сложной сериализацией. Часто встречаются компактные форматы в виде потока бит.
EP>В принципе да, но также есть и целые числа переменной длины — чтобы знать где что находится, нужно как минимум распарсить такие числа и т.п.
Тот факт, что их нужно распарсить, не отменяет возможности рассматривать их как массив байтов переменной длины, поэтому сводится к предыдущему.
Теоретически я вообще не вижу ситуации, когда я не могу хранить все данные некоторого объекьа в мной выделенном блоке памяти. Не могу по той простой причине, что не все ли равно где байты эти хранятся, а раз все равно, то пусть они в моем блоке и хранятся. Разумеется, такой подход требует полного контроля над аллокацией памяти.
Практически — это может быть совсем не всегда легко сделать, и не всегда применимо.
EP>Меня больше интересует общий механизм, со сложной сериализацией. Часто встречаются компактные форматы в виде потока бит.
Любой набор последовательно расположенных данных всегда можно рассматривать как массив байтов переменной длины
Здравствуйте, niXman, Вы писали:
X>что-то я уже запутался %) X>сейчас, какие остались вопросы?
Мне интересна практика — кто как решал подобную задачу. Если есть интересные техники — с удовольствием послушаю.
Судя по топику — такая задача встречается редко: либо потому что размер сообщений маленький, либо текущей производительности достаточно, либо не замечается возможность оптимизации.
Самое близкое что есть из стандартных решений — XML/JSON SAX.
X>как я понял, пока что нет вариантов асинхронной сериализации без использования корутин?
Судя по всему stackful coroutines предоставляют самый удобный способ.
Альтернативы есть, но более громоздкие. Например на основе Boost.Fusion (для сериализации каких-то custom типов нужно вручную писать автомат, но когда все базовые типы есть — дерево структуры определяется элементарно) или кодогенерации (готовых решений вроде бы нет. думаю при желании можно добавить шаблоны в Thrift/Protobuf).
Здравствуйте, Pavel Dvorkin, Вы писали:
EP>>В принципе да, но также есть и целые числа переменной длины — чтобы знать где что находится, нужно как минимум распарсить такие числа и т.п. PD>Тот факт, что их нужно распарсить, не отменяет возможности рассматривать их как массив байтов переменной длины, поэтому сводится к предыдущему.
Полезность такого сырого массива байт иногда близка к нуля, так как необходимые операции будут очень дорогими. Пример ниже.
PD>Теоретически я вообще не вижу ситуации, когда я не могу хранить все данные некоторого объекьа в мной выделенном блоке памяти. Не могу по той простой причине, что не все ли равно где байты эти хранятся, а раз все равно, то пусть они в моем блоке и хранятся. Разумеется, такой подход требует полного контроля над аллокацией памяти. PD>Практически — это может быть совсем не всегда легко сделать, и не всегда применимо.
На C++ это как раз не так трудно реализуется — просто используется custom deleter.
EP>>Меня больше интересует общий механизм, со сложной сериализацией. Часто встречаются компактные форматы в виде потока бит. PD>Любой набор последовательно расположенных данных всегда можно рассматривать как массив байтов переменной длины
Пример:
Есть int64 который нужно хранить в переменном количестве байт (допустим от 1 до 9 байт). И есть сырой сериализованный массив состоящий из таких целых переменной длинны.
Если десериализовать его в нормальный массив int64[], то получим O(1) доступ по индексу. Если же работать с ним как с сырым массивом (и без построения дополнительных вспомогательных структур) — то доступ по индексу становится O(N).
И это самый простой вариант. Если же есть древовидная структура с полями и массивами переменной длинны, причём на разных уровнях, да и ещё и сжатая чем-то типа кода Хаффмана — то всё становится намного труднее.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Пример: EP>Есть int64 который нужно хранить в переменном количестве байт (допустим от 1 до 9 байт). И есть сырой сериализованный массив состоящий из таких целых переменной длинны. EP>Если десериализовать его в нормальный массив int64[], то получим O(1) доступ по индексу. Если же работать с ним как с сырым массивом (и без построения дополнительных вспомогательных структур) — то доступ по индексу становится O(N).
Я же вообще не предлагал сериализовать. Храни как хочешь, только в непрерывном куске. Хочешь — храни в нем нормальный int64[], хочешь — массив байтов некоего размера с упакованными int64, а за ним дополнительные вспомогательные структуры. Что хочешь, то и храни. А потом отправь этот блок памяти куда следует целиком.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, uzhas, Вы писали:
EP>>>Основной фокус как раз тут. EP>>>Вот вызвали async_write для первого чанка. Следующий async_write должен происходить из хэндлера записи.
U>>Почему должен?
EP>Во-первых, порядок двух асинхронных операций не определён. Во-вторых, async_write не атомарен, и сам состоит из нескольких операций типа async_write_some, которые могут перемешиваться с async_write_some от другого async_write. EP>Но даже если у нас есть строгий порядок асинхронных записей (допустим у нас есть очередь (переменного размера, что уже не хорошо)) — то всё равно не понятно как это будет работать:
И вообще cдался этот boost::asio? Можно серилизовать с boost::serialization или чем-то готовым вроде protobuf, в отдельных потоках
или процессах, если процессоров несколько. А ввод/вывод производить в в отдельном потоке нативным
средством асинхронного ввода/вывода ОС, обёрткой над которым является boost::asio,( в linux epoll).
Этот же поток ввода/вывода будет прозводить запуск новых агентов сериализации, и взаимодействовать с запущенными,
передавать данные с очереди, которую будут заполнять агенты. Интересно узнать причины использовани именно boost::asio.
Здравствуйте, smeeld, Вы писали:
S>И вообще cдался этот boost::asio? Можно серилизовать с boost::serialization или чем-то готовым вроде protobuf,
Вообще-то boost::asio и boost::serialization ортогональны. Более того — они прекрасно работают вместе.
При использовании Boost.Serialization/Protobuf — будут ровно те же проблемы. То есть их можно использовать в лоб, с буферами любых размеров (которые и так прекрасно пересылаются Boost.Asio), что многие и делают — но это неэффективно. Эффективней использовать буфер фиксированного размера и сериализовать по частям.
S>в отдельных потоках или процессах, если процессоров несколько. А ввод/вывод производить в в отдельном потоке нативным S> средством асинхронного ввода/вывода ОС, обёрткой над которым является boost::asio,( в linux epoll). S>Этот же поток ввода/вывода будет прозводить запуск новых агентов сериализации, и взаимодействовать с запущенными, S>передавать данные с очереди, которую будут заполнять агенты.
Соединений десятки тысяч. Десятки тысяч потоков/процессов не взлетят.
S>Интересно узнать причины использовани именно boost::asio.
Asio не принципиально — подойдёт любой асинхронный/неблокирующий вывод — и везде будут ровно те же проблемы.
Здравствуйте, smeeld, Вы писали:
S>А ввод/вывод производить в в отдельном потоке нативным S>средством асинхронного ввода/вывода ОС, обёрткой над которым является boost::asio,( в linux epoll).
ну да, можно и вовсе на Си писать =)
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Сериализация быстрее I/O — что делать когда все буферы заполнены, но ещё не отправлены? (суммарный размер буферов меньше самой структуры — мы же экономим)
тут несколько может быть стратегий: выделить еще буфера, уснуть, пока буфер не освободиться, выдать ошибку (no resources)
EP>В FIX'е какой максимальный/типичный размер сообщения?
тут мне тяжело за весь протокол ответить, в нашем сервере на практике это где-то 16K / 2K
EP>Для десериализации структуры должно быть вызвано несколько on_data, так как буфер фиксированный, а структура в него не помещается.
правильно
EP>Как процедура сериализации уйдёт в сон пока ожидает следующую on_data? Как продолжит десереализацию в правильном состоянии когда придут данные?
аналогией является SAX парсер : данные пихаются снаружи, а не он тянет их откуда-то, поэтому о сне говорить не приходится. внутри обычно машина состояний живет
Здравствуйте, niXman, Вы писали:
X>а чо, YAS не рассматривается?
Рассматривался — я давно его видел, отложился в памяти как альтернатива Boost.Serialization.
В данном случае для сериализации нужны только правильные архивы, которые в любом случае писать — что для YAS, что для Boost.Serialization. Никакие дополнительные фичи типа отслеживания указателей или сериализации полиморфных типов не нужны.
Сейчас всё ядро сериализации (без YAS/Boost.Serialization) — это in/out archive + рекурсивная сериализация Boost.Fusion Forward Sequence. На всё про всё — меньше ста строк, причём 3/4 это архивы.
Структуры определяются просто как BOOST_FUSION_DEFINE_STRUCT и уже готовы к сериализации.
Здравствуйте, uzhas, Вы писали:
EP>>Сериализация быстрее I/O — что делать когда все буферы заполнены, но ещё не отправлены? (суммарный размер буферов меньше самой структуры — мы же экономим) U>тут несколько может быть стратегий: выделить еще буфера, уснуть, пока буфер не освободиться, выдать ошибку (no resources)
Основной вопрос в том, как организовать засыпание.
EP>>В FIX'е какой максимальный/типичный размер сообщения? U>тут мне тяжело за весь протокол ответить, в нашем сервере на практике это где-то 16K / 2K
Я думаю при таких размерах нет смысла делить сериализацию одного 16K сообщения на чанки. То есть можно просто целиком сериализовать в буфер, и этот буфер отправить одним async_write.
EP>>Как процедура сериализации уйдёт в сон пока ожидает следующую on_data? Как продолжит десереализацию в правильном состоянии когда придут данные? U>аналогией является SAX парсер : данные пихаются снаружи, а не он тянет их откуда-то, поэтому о сне говорить не приходится. внутри обычно машина состояний живет
Ну да, нужна машина состояний. Вопрос в том, какими способами это машину состояний можно определить. Пока что stackful coroutine — самый удобный.
Здравствуйте, uzhas, Вы писали:
U>есть еще такой вариант: если разным клиентам раздаются одинаковые данные, то проще их закешировать результат сериализации) и не делать часто одну и ту же сериализацию
Да — это хороший вариант для отправки.
Но в этом конкретном случае не только отправка больших сообщений, но также и приём
Здравствуйте, Evgeny.Panasyuk, Вы писали: EP>В данном случае для сериализации нужны только правильные архивы, которые в любом случае писать — что для YAS, что для Boost.Serialization.
что это значит?
EP>рекурсивная сериализация Boost.Fusion Forward Sequence.
YAS это умеет искаропки.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Собственно у меня два вопроса: EP>1. Какие есть ещё альтернативы? Возможно есть какие-то стандартные/распространённые решения? EP>2. Встречалась ли вам такая задача? В каком контексте? И как она была решена?
В вашем несколько сумбурном описании мне слышится, по крайней мере, две ортогональные задачи:
1. Собственно, преобразование форматов между внутренним представлением и внешним представлением.
2. Как реализовать это преобразование в условиях, когда за один прием вся древовидная структура в сокет не лезет.
Вторая задача решается довольно просто, если у вас есть некий итератор по этой древовидной структуре, который умеет по запросу выдавать следующий элемент. Тогда вы просто, по мере готовности сокета, обходите свою структуру с помощью этого итератора, и по кусочкам проталкиваете в сеть.
Что до первой задачи, вы слишком мало рассказали про свою структуру, чтобы к ней хоть как-то отнестись.
Да, и учтите пожалуйста, что зачастую выгоднее потратить пару мегабайт памяти, чем изобретать хитроумные алгоритмы, позволяющие этого не делать. Объем памяти (в разумных, конечно, пределах) — гораздо более дешевый ресурс, чем производительность процессора.
Здравствуйте, niXman, Вы писали:
X>вы либо прикалываетесь, либо троллите, либо просто глупы...
Ок, можно и boost::asio-элегантно получается, если расписывать логику accept-ирования
входящих соединений классами, без С-подобных нагромождений в стиле linux kernel source code. X>и что? банальный 'operator new' есть обертка. его тоже лучше не юзать?
Ну, new не критичен, можно и оставить. X>не представляю, какие вещества нужно употреблять, что писать тут такое %)
Не пью, не курю, занимаюсь спортом.
Здравствуйте, smeeld, Вы писали:
A>>почему *желательно*, обоснуйте. S>Можно задать встречный вопрос? S>Вы смотрели на ассемблерные листинги программ на C++ и C?
да, приходилось когда занимался реверсированием.
однако на асм своего кода обычно смотреть не нужно, С и С++ довольно просто компилируются в уме.
S>Если тот кошмар, который генерит компилятор с C++ по сравнению с тем, что получается с C подобного синтаксиса S>способствует снижению латентности, S> то этот мир не совершенен больше, чем предпологалось.
согласен, особенно страшный код генерится из Boost.MPL — шаблонные меташтуки, это же квинтэссенция С++, правда?
может Вы поделитесь с нами какими-то конкретными примерами? типа "вот код с одинаковой функциональностью и одинаковыми свойствами, у С++ кода такой результат компиляции, а у С — лучше"
Здравствуйте, niXman, Вы писали:
EP>>В данном случае для сериализации нужны только правильные архивы, которые в любом случае писать — что для YAS, что для Boost.Serialization. X>что это значит?
Данные должны быть в строго определённом формате, с правильным порядком байт.
EP>>рекурсивная сериализация Boost.Fusion Forward Sequence. X>YAS это умеет искаропки.
Я видел. У меня это меньше 15 строк. Там считай только один вызов fusion::for_each или fusion::fold.
Здравствуйте, Pzz, Вы писали:
Pzz>Вторая задача решается довольно просто, если у вас есть некий итератор по этой древовидной структуре, который умеет по запросу выдавать следующий элемент. Тогда вы просто, по мере готовности сокета, обходите свою структуру с помощью этого итератора, и по кусочкам проталкиваете в сеть.
Так вопрос как раз в том, как этот итератор для древовидной структуры сделать. Учитывая то, что узлы структуры гетерогенные.
Pzz>Что до первой задачи, вы слишком мало рассказали про свою структуру, чтобы к ней хоть как-то отнестись.
Мне интересна любая практика асинхронной сериализации. Если есть какой-то пример — с удовольствием послушаю, независимо от структуры (часть вопроса как раз в том, какие структуры для этого лучше подходят).
Pzz>Да, и учтите пожалуйста, что зачастую выгоднее потратить пару мегабайт памяти, чем изобретать хитроумные алгоритмы, позволяющие этого не делать.
Соединений десятки тысяч. Буфер для одного соединения может занимать до нескольких мегабайт. В среднем случае экономия порядка гигабайт.
Pzz>Объем памяти (в разумных, конечно, пределах) — гораздо более дешевый ресурс, чем производительность процессора.
"Да и вообще всё базу упирается!"
Если серьёзно, то в общем случае при меньшем использовании памяти:
1. есть возможность полностью поместится в L3, что является огромным boost'ом для производительности.
2. меньше вероятность cache miss
3. свободную память можно использовать для разного рода кэшей, а-ля memcached, или кэш самой OS
4. свободную память можно использовать для space–time tradeoff — например отдавать хэштаблицам много места, чтобы реже получать коллизии.
Здравствуйте, smeeld, Вы писали:
S>а вот для организации ввода/вывода не прозрачней будет вместо boost::asio использовать, например, в линуксе нативный epoll?
Что это даст?
S>Сама boost::asio, та её часть, что относится к асинхронному вводу/выводу есть обёртка над epoll в линуксе. S>Понятно, что потеряется кроссплатформенность, но при использование простого api epoll проще будет реализовать S>сложную логику последовательной, растянутой во времени передачи сериализованных данных частями в несколько S>тысяч потоков чем эти async_read/async_write которые обязательно поведут себя неизветно как.
С async_read/async_write нет никаких проблем — всё удобно и прозрачно. Проблемы начнутся если без нужды вручную использовать epoll.
К тому же в контексте асинхронной сериализации, epoll не добавляет ничего что помогло бы решить проблему.
EP>>Соединений десятки тысяч. Десятки тысяч потоков/процессов не взлетят. S>Есть спасение, берём четырёхпроцовый сервак, несколко, с балансировкой между ними.
Зачем, если и так всё прекрасно работает?
S>А как тогда взлетают веб сервисы? Асинхронная обработка в один поток годится только если, собственно, S>обработки не много. В противном случае пускаем новый поток/процесс. Связки однопоточный nginx в frontend для распределения запросов и многопроцессный apache для обработки запросов в backend-стандарт для hayload.
Смотрим на node.js, erlang, да и вообще общий тренд к использованию зелёных потоков.
EP>>Asio не принципиально — подойдёт любой асинхронный/неблокирующий вывод — и везде будут ровно те же проблемы. S>Вот и я о том же, используем более предсказуемые нативные для ОС api kqueue (freebsd), epoll (linux), не знаю S>что там есть по этому поводу в win.
Переход на ручной epoll не решит никаких проблем с асинхронной сериализацией.
Здравствуйте, smeeld, Вы писали:
A>>почему *желательно*, обоснуйте. S>Можно задать встречный вопрос? S>Вы смотрели на ассемблерные листинги программ на C++ и C? S>Если тот кошмар, который генерит компилятор с C++ по сравнению с тем, что получается с C подобного синтаксиса S>способствует снижению латентности, S> то этот мир не совершенен больше, чем предпологалось.
Я провёл сотни часов смотря на том что генерирует компилятор. И в контексте RE, и в контексте оптимизации.
C++ позволяет компиляторам генерировать намного более быстрый код чем C. Хотя бы потому, что язык не выбрасывает всю полезную информацию так необходимую для оптимизации.
Классический пример — std::sort vs qsort. Какой интерфейс более простой, гибкий, и позволяет генерировать оптимальный код?
P.S. Это уже offtopic, лучше перейти в другую тему. Тем более сто раз уже разжёвывалось.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
Pzz>>Вторая задача решается довольно просто, если у вас есть некий итератор по этой древовидной структуре, который умеет по запросу выдавать следующий элемент. Тогда вы просто, по мере готовности сокета, обходите свою структуру с помощью этого итератора, и по кусочкам проталкиваете в сеть.
EP>Так вопрос как раз в том, как этот итератор для древовидной структуры сделать. Учитывая то, что узлы структуры гетерогенные.
Ну, если представить себе, что узлы структуры состоят из каких-то собственных данных и ссылок на подузлы, добавляем ссылку назад и храним в итераторе текущий узел, номер текущей ссылки на подузел и, если собственные данные достаточно большие, текущее положение в них.
Соответственно, когда итератор просят дать следующую порцию, он либо выдает очередной кусок данных узла, либо идет вглубь по следующей ссылке, либо, если больше из этого узла выжать нечего, возвращается назад, и в том узле берет следующую ссылку на подузел. Когда идти больше некуда, значит, мы обошли все дерево.
EP>Если серьёзно, то в общем случае при меньшем использовании памяти: EP>1. есть возможность полностью поместится в L3, что является огромным boost'ом для производительности.
Скорее всего, вы на этом мало чего наиграете. Эта память, в которую вы один раз кладете, и один раз забираете. Причем забираете со скоростью сокета, т.е., весьма неторопясь. Пока за данными придут, из кэша они давно уже вылетят.
EP>2. меньше вероятность cache miss EP>3. свободную память можно использовать для разного рода кэшей, а-ля memcached, или кэш самой OS EP>4. свободную память можно использовать для space–time tradeoff — например отдавать хэштаблицам много места, чтобы реже получать коллизии.
5. Свободную память можно продать на радиорынке, а деньги пропить
Здравствуйте, Pzz, Вы писали:
Pzz>>>Вторая задача решается довольно просто, если у вас есть некий итератор по этой древовидной структуре, который умеет по запросу выдавать следующий элемент. Тогда вы просто, по мере готовности сокета, обходите свою структуру с помощью этого итератора, и по кусочкам проталкиваете в сеть. EP>>Так вопрос как раз в том, как этот итератор для древовидной структуры сделать. Учитывая то, что узлы структуры гетерогенные. Pzz>Ну, если представить себе, что узлы структуры состоят из каких-то собственных данных и ссылок на подузлы, добавляем ссылку назад и храним в итераторе текущий узел, номер текущей ссылки на подузел и, если собственные данные достаточно большие, текущее положение в них.
В этом варианте много рантайм оверхеда — как минимум потому, что типы узлов разные (а это тянет за собой type erasure).
Если делать древовидную структуру, то тогда уже на Boost.Fusion, как я описал в первом сообщении.
EP>>Если серьёзно, то в общем случае при меньшем использовании памяти: EP>>1. есть возможность полностью поместится в L3, что является огромным boost'ом для производительности. Pzz>Скорее всего, вы на этом мало чего наиграете. Эта память, в которую вы один раз кладете, и один раз забираете. Причем забираете со скоростью сокета, т.е., весьма неторопясь. Пока за данными придут, из кэша они давно уже вылетят.
Когда вся используемая память помещается в L3 — в обычную RAM не надо ходить, а это дорогого стоит.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Когда вся используемая память помещается в L3 — в обычную RAM не надо ходить, а это дорогого стоит.
думается мне, ты слишком заигрался с оптимизацией
страшно представить, чтоб мне ставили такие ограничения на используемую память %)
соединений тоже десятки тысяч, но никаких ограничений на кол-во и размер буферов.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, smeeld, Вы писали:
S>Здравствуйте, Abyx, Вы писали:
A>>почему *желательно*, обоснуйте. S>Можно задать встречный вопрос? S>Вы смотрели на ассемблерные листинги программ на C++ и C? S>Если тот кошмар, который генерит компилятор с C++ по сравнению с тем, что получается с C подобного синтаксиса S>способствует снижению латентности, S> то этот мир не совершенен больше, чем предпологалось.
А можно на конкретных примерах? Просто чаще всего, С и С++ компиляторы имеют разные фронтенды и один и тот же backend, занимающийся оптимизацией.
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, smeeld, Вы писали:
S>>Вы смотрели на ассемблерные листинги программ на C++ и C? X>я — смотрел. и да, С++ компилятор генерит более качественный код, и оптимизирует лучше. X>а что я, по вашему, должен был там еще увидеть?
Так нельзя сказать в общем случае. Иногда С++ компилятор генерирует менее качественный код, а иногда — более качественный. Идентичный код они вообще будут компилировать одинаково. А еще все очень сильно зависит от приложения и опций компилятора.
Здравствуйте, Lazin, Вы писали:
L>Так нельзя сказать в общем случае. Иногда С++ компилятор генерирует менее качественный код, а иногда — более качественный. Идентичный код они вообще будут компилировать одинаково. А еще все очень сильно зависит от приложения и опций компилятора.
Общий случай не интересен. Действительно на C можно сделать код таким же быстрым как и C++. Дело в цене такого кода.
Элементарный пример:
template<typename I, typename P>
I find_if(I first, I last, P p)
{
while(first != last)
{
if(p(*first))
return first;
++first;
}
return first;
}
такой линейный поиск работает с:
любыми типами последовательностей
любыми типами элементов
любыми предикатами
Попробуй получить такую же гибкость на C не потеряв производительность.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Мне интересна любая практика асинхронной сериализации. Если есть какой-то пример — с удовольствием послушаю, независимо от структуры (часть вопроса как раз в том, какие структуры для этого лучше подходят).
Я думаю стоит искать и спрашивать не асинхронной сериализацией, ибо это какой-то франкенштейн, а инкрементальной. Думаю google в этом случае будет отдавать куда более релевантные результаты. Вот скажем некоторые ASN.1 компиляторы умеют генерировать код, способный инкрементально сериализовать и десериализовать данные.
Асинхронность в этом случае прикручивается просто, например на тех же stackfull coroutines — насериализовали данных в буфер, отправили, насериализовали еще и тд, пока всю структуру не отправили.
А асинхронная сериализация это какой-то булшит. У меня первая ассоциация — сериализация в отдельном потоке, асинхронно основному потоку выполнения.
EP>Соединений десятки тысяч. Буфер для одного соединения может занимать до нескольких мегабайт. В среднем случае экономия порядка гигабайт.
Ну на самом деле не факт. Тут все зависит от конкретных чисел. Нужно смотреть на пропускную способность разных подсистем, ведь одновременно, приложение не может обслуживать все эти десятки тысяч соединений, оно не может одновременно сериализовать много данных, скорее всего оно будет сериализовать numcpu*const структур данных в секунду, а потом асинхронно отправлять их. И далеко не факт, что если получится сделать инкрементальную сериализацию больших структур данных, пропускная способность приложения увеличится. Это, вероятно, позволит "кормить" одномоментно кормить данными больше соединений, но медленнее
Здравствуйте, Lazin, Вы писали:
EP>>Попробуй получить такую же гибкость на C не потеряв производительность. L>Это уже вопрос культурных различий L>Вполне возможно, этот шаблон будет инстанциирован для двух-трех разных типов и его вполне возможно написать на Си несколько раз
В том то и дело, что на C обычно пишут с использованием runtime полиморфизма — и не парятся, отсюда и все тормоза.
Типичный пример — GLib:
Или тот же qsort из стандартной библиотеки.
L>И вот этот код, вручную специализированый, будет работать быстрее, так как нет исключений, например, и не нужно сохранять данные для разворачивания стека в случае возникновения исключения.
1. Исключения использовать не обязательно.
2. За счёт inline'инга компилятор может увидеть что там нет никаких исключений.
3. На x64 используются zero-cost exceptions. Проверка кодов ошибок по всему callpath может быть даже медленней на happy-path чем исключения.
L>Для С++ программиста это звучит странно, но тем не менее, так написано большое количество хорошего кода.
В первую очередь "полезного" кода. Плохой код тоже может выполнять полезные функции.
Здравствуйте, Lazin, Вы писали:
L>Я думаю стоит искать и спрашивать не асинхронной сериализацией, ибо это какой-то франкенштейн, а инкрементальной. Думаю google в этом случае будет отдавать куда более релевантные результаты. Вот скажем некоторые ASN.1 компиляторы умеют генерировать код, способный инкрементально сериализовать и десериализовать данные.
Хм. "инкрементально" — первая ассоциация это incremental backup, когда записывается цепочка diff'ов.
Да и google выдает нечто похожее по incremental serialization — сериализацию последних изменений.
L>Асинхронность в этом случае прикручивается просто, например на тех же stackfull coroutines — насериализовали данных в буфер, отправили, насериализовали еще и тд, пока всю структуру не отправили.
Stackful coroutines везде прикручиваются легко.
Например их без проблем можно использовать с Boost.Serialization, в котором нет никакой асинхронной/инкрементальной сериализации.
L>А асинхронная сериализация это какой-то булшит. У меня первая ассоциация — сериализация в отдельном потоке, асинхронно основному потоку выполнения.
Если бы я знал правильные keywords, которые привели бы меня туда, где есть практические примеры — я бы наверное тут и не спрашивал
EP>>Соединений десятки тысяч. Буфер для одного соединения может занимать до нескольких мегабайт. В среднем случае экономия порядка гигабайт. L>Ну на самом деле не факт. Тут все зависит от конкретных чисел. Нужно смотреть на пропускную способность разных подсистем, ведь одновременно, приложение не может обслуживать все эти десятки тысяч соединений, оно не может одновременно сериализовать много данных,
Отчего же? На современных машинах bandwidth памяти ~30GiB/s — это upper bound для сериализации.
Даже если сериализация будет жутко тормозной — сеть всё равно получится забить.
о, вспомнил!
у тебя же буфер фиксированного размера, так?
насколько я помню, ты собирался переключать контекст после сериализации каждого мембера?
а как быть, если какой-то мембер по объему не вмещается в буфер? переключать контекст еще и при заполнении буфера, прям посреди его сериализации?
ты вот честно скажи, задача реальная или придуманная для разминки? и на каком девайсе твой код должен работать?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
X>о, вспомнил! X>у тебя же буфер фиксированного размера, так?
Да — ради этого всё и затевается.
X>насколько я помню, ты собирался переключать контекст после сериализации каждого мембера?
Нет, только после того как буфер заполнится.
X>а как быть, если какой-то мембер по объему не вмещается в буфер? переключать контекст еще и при заполнении буфера, прям посреди его сериализации?
Контекст планируется переключать только при заполнении буфера.
То есть внутри архива при каждом добавлении (либо считывании — в случае десереализации) будет проверка на заполненность. Как только полностью забили буфер — делаем async_write + yield.
X>ты вот честно скажи, задача реальная или придуманная для разминки?
Весь проект "для разминки", не только эта часть.
Я сначала сделал в лоб — с буфером переменной длинны. И пока ещё не упёрся ни в какие ограничения производительности (точнее нагрузка была небольшой).
Но во-первых это перерасход памяти (так как размер сообщений может быть большим), а во-вторых это лишняя динамическая аллокация.
Раз есть возможность для оптимизации — то наверняка кто-то уже пытался её реализовать (особенно учитывая то, что я сетевыми приложениями никогда не занимался профессионально и сразу наткнулся на эту возможность).
Собственно меня и интересует best practices.
X>и на каком девайсе твой код должен работать?
Хотелось бы на каком-нибудь 2xCPU Haswell'e. Но сначала попробую на чём-то типа Amazon EC2 c3.8xlarge.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Отчего же? На современных машинах bandwidth памяти ~30GiB/s — это upper bound для сериализации.
Ну это в крайнем случае, если данные читаются последовательно и пишутся последовательно. На практике, буферы будут в разных, не пересекающихся участках памяти, структуры данных для сериализации — тоже иерархические, а значит в памяти непоследовательно расположены, а значит тоже будут читаться медленно, думаю — пара сотен мегабайт, это верхний предел для такого
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Нет, только после того как буфер заполнится.
а если в буфер помещается только часть мембера, как быть?
EP>Но во-первых это перерасход памяти (так как размер сообщений может быть большим)
перерасход памяти для приложений этого класса, думается мне, это начиная с 256ГБ
EP>а во-вторых это лишняя динамическая аллокация.
заведи пул буферов. не нужно постоянно выделять.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, 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 не нужно и можно просто освобождать ресурсы тогда, когда они не нужны.
Здравствуйте, 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>в сессии храни итератор, и по хендлеру записи инкрементируй его.
Так весь вопрос в том, как этот итератор запрограммировать
Здравствуйте, 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.
И это компилируется в компактный простой ассемблерный листинг с jmp/jne/je.. скачками в пределах файла.
В случае же использования С++ try/catch сплошные call XXXXXXX
в дебри либ и фреймворков, которые вызываются по ходу выполнения потока, постоянно прерывая его.
AF>Раньше были спецы, про которых говорили: "пишут не на C++, а на C с классами". AF>Теперь пора пускать в обиход выражение: "пишут на C, как на C++ без классов"
Вся прога на С++, с критичными участками на C++ без его классов и ништяков, которые
не бесплатны, и забирают память и процессор.
Здравствуйте, andrew.f, Вы писали:
EP>>Так мы же про общий случай, сортировка — это просто иллюстрация. AF>qsort какой то неудачный пример — функция стандартная, и ей "сто лет в обед",
AF>и есть масса других вариантов библиотек сортировки.
Покажи хотя бы пример интерфейса сортировки на C, которая будет принимать любой предикат, любой тип последовательности, с любыми элементами.
AF>В контр-пример можно привести, что printf в большинстве случаев работает быстрее std::cout (можно глянуть на тесты boost::format — там это замечательно видно).
Пример не равноценный — std::cout медленный потому что много чего делает, и имеет customization points.
А qsort медленный — потому что интерфейс такой
AF>На C можно тоже исходники чистыми хедерниками писать. AF>И втюхивать в начало каждой функции, как inline, так и static inline, ну и (непотребный конечно вариант, но все таки хоть какая то замена шаблонам из C++) #define. AF>На выходе получишь те же самые свернутые в одну функцию полезные действия.
Покажи интерфейс сортировки, или например rbtree.
AF>И опять же под линуксом Firefox (всеми горячо любимым gcc) официально собирается с -Os, и он правда в такой варианте сборки работает быстрее, чем с -O2/-O3.
Здравствуйте, andrew.f, Вы писали:
EP>>Фишка в том, что в случае исключений ему даже угадывать ничего не придётся — cmp+je/whatever не будут разбросаны по всему happy path. AF>Какая то подтасовка фактов тут описана.
Где?
AF>Проверка на неудачно совершенную операцию в случае с C++ exception все равно есть.
Очевидно, что если такая проверка только в самом низу call-tree, то в случае исключений будет один if. В то время как на C error_code придётся протаскивать наверх через всю call-tree-branch, по всему объёму дерева
AF>И если операция совершилась неудачно, только в этом случае происходит throw. AF>Видимо имеется ввиду, когда произошла куча вызовов функций в глубину, и на каждом возврате из вызова функции происходит проверка результата?
Да.
AF>Но ведь это замечательно рефакторится (и без всяких goto) разбиением общей функции с ветвлениями на две: AF>- первая функция последовательные действия с проверкой результата от одиночного действия и в случае неудачи возврат во вторую функцию. AF>- вторая функция проверяет результат первой и делает то что надо.
Во-первых у тебя остались те же jump'ы — switch/case, причём даже менее эффективные чем goto.
Во-вторых от того что ты спрятал runtime проверки в отдельную функцию — они быстрее не стали. Они всё также остались разбросанными по всему happy-path.
Вот смотри, в случае исключений будет:
if(error)
throw something();
в самом низу call-tree, у тебя же будут проверки (runtime ветвления) по всему call-tree
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Во-первых у тебя остались те же jump'ы — switch/case, причём даже менее эффективные чем goto. EP>Во-вторых от того что ты спрятал runtime проверки в отдельную функцию — они быстрее не стали. Они всё также остались разбросанными по всему happy-path. EP>Вот смотри, в случае исключений будет: EP>
EP>if(error)
EP> throw something();
EP>
в самом низу call-tree, у тебя же будут проверки (runtime ветвления) по всему call-tree
if-ы и switch для редко выполняющихся условий, это практически zero overhead. Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch. Т.е. код, выбирающий обработчик для кода ошибки у компилятора получится более быстрым, нежели код для try catch.. catch.. catch, причем намного. Я уже молчу про локальность этого кода
Здравствуйте, Lazin, Вы писали:
L>if-ы и switch для редко выполняющихся условий, это практически zero overhead.
Этот "практически zero overhead" будет разбросан по всему коду (который к тому же забивает prediction table).
L>Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch.
switch table будет медленнее оригинальных goto. Я вообще не вижу смысл в переводе безусловного goto на switch в контексте обсуждения производительности
L>Т.е. код, выбирающий обработчик для кода ошибки у компилятора получится более быстрым, нежели код для try catch.. catch.. catch, причем намного. Я уже молчу про локальность этого кода
Возможно выбор действия при ошибке будет и быстрее, но по всему happy-path размазывается penalty в виде if'ов.
Здравствуйте, niXman, Вы писали:
L>>if-ы и switch для редко выполняющихся условий, это практически zero overhead. Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch. Т.е. код, выбирающий обработчик для кода ошибки у компилятора получится более быстрым, нежели код для try catch.. catch.. catch, причем намного. Я уже молчу про локальность этого кода
X>т.е. ты хочешь сказать, что обработка исключительных ситуаций это штатное выполнение программы?
Я ничего такого не хотел сказать. Посмотри на что я отвечал и что я написал в ответ. Мой поинт в том, что runtime проверки кодов ошибок могут вообще не сказываться на времени работы кода, все. Зачем обобщать?
Здравствуйте, Evgeny.Panasyuk, Вы писали:
L>>Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch. EP>switch table будет медленнее оригинальных goto. Я вообще не вижу смысл в переводе безусловного goto на switch в контексте обсуждения производительности
Я не предлагаю заменять goto на switch. Просто сравниваю два способа обработки ошибок — throw catch и код ошибки + switch для последующей обработки.
L>>Т.е. код, выбирающий обработчик для кода ошибки у компилятора получится более быстрым, нежели код для try catch.. catch.. catch, причем намного. Я уже молчу про локальность этого кода EP>Возможно выбор действия при ошибке будет и быстрее, но по всему happy-path размазывается penalty в виде if'ов.
Несомненно размазывается. Я не утверждаю, что коды ошибок всегда быстрее, я лишь утверждаю что можно написать так, что никакого penalty не будет, я даже небольшой пример написал
#include <iostream>
#include <boost/timer.hpp>
using namespace std;
int main()
{
const int num_samples = 200000000;
float* A = new float[num_samples];
float* B = new float[num_samples];
float* C = new float[num_samples];
boost::timer timer;
for (int i = 0; i < num_samples; i++) {
A[i] = 0.f;
B[i] = 1.f;
C[i] = 0;
}
double result = timer.elapsed();
std::cout << "rewrite of " << (3*num_samples*sizeof(float)/(1024*1024)) << " Mb takes " << result << std::endl;
timer.restart();
for (int i = 0; i < num_samples; i++) {
C[i] += A[i] + B[i];
}
result = timer.elapsed();
std::cout << "without branch: " << result << std::endl;
timer.restart();
for (int i = 0; i < num_samples; i++) {
if (i == -1)
break;
if (i == -2)
break;
if (i == -3)
break;
if (i == -4)
break;
if (i == -5)
break;
if (i == -6)
break;
if (i == -7)
break;
if (i == -8)
break;
if (i == -9)
break;
C[i] += A[i] + B[i];
}
result = timer.elapsed();
std::cout << "with branch: " << result << std::endl;
}
На моей машине, оба цикла отрабатывают за одно и то же время, по причине того, что branch predictor успешно предсказывает все ветвления в коде, по причине того, что i никогда не принимает отрицательные значения.
Здравствуйте, Lazin, Вы писали:
L>На моей машине, оба цикла отрабатывают за одно и то же время, по причине того, что branch predictor успешно предсказывает все ветвления в коде, по причине того, что i никогда не принимает отрицательные значения.
Хотя возможно, причина в том, что оптимизатор сводит это все к одному branch-у, я не смотрел disassembly.
Здравствуйте, Lazin, Вы писали:
L>>>Мало того, switch для кодов ошибок (когда коды ошибок выбраны правильно), компилится в switch table, который работает намного быстрее чем throw catch. EP>>switch table будет медленнее оригинальных goto. Я вообще не вижу смысл в переводе безусловного goto на switch в контексте обсуждения производительности L>Я не предлагаю заменять goto на switch. Просто сравниваю два способа обработки ошибок — throw catch и код ошибки + switch для последующей обработки.
В реальности есть не один код ошибки, а много — и вместо switch нужен goto.
L>На моей машине, оба цикла отрабатывают за одно и то же время, по причине того, что branch predictor успешно предсказывает все ветвления в коде, по причине того, что i никогда не принимает отрицательные значения.
Во-первых у тебя memory-bounded вычисления — большую часть времени будет занимать пересылка данных в/из памяти. Чтобы обойти этот эффект, попробуй сделать num_samples = 64k (чтобы попасть хотя бы в L3), и добавить внешний цикл "repeat".
Во-вторых попробуй добавить volatile на i, чтобы отключить оптимизатор.
И в-третьих тут один цикл с несколькими проверками — тут branch predictor должен неплохо сработать. В реальном же коде, эти проверки будут размазаны тонким слоем по всему call graph, и не всегда будут вызываться в циклах. Плюс они забивают predictor table.
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, Evgeny.Panasyuk, Вы писали:
AF>>В контр-пример можно привести, что printf в большинстве случаев работает быстрее std::cout (можно глянуть на тесты boost::format — там это замечательно видно). X>что за бред?! X>с каких таких пор эта сишная всезаглатывающая кишка стала быстрее плюсовых потоков?! X>
X>да и как вообще может быть такое, чтоб printf() который вообще ничего не знает о типе, выполнялся быстрее типизированного кода?
Это такой забавный троллинг? Или Вы серьезно не умеете оптимизировать под разные типы системных вызовов?
Вот, я ни в коем случае не хаю C++ — я на нем реально пишу очень и очень много. Но подходы к написанию C-кода и C++-кода в реалиях очень сильно разные, в отличии от тестовых задач, написанных в лоб в одном и том же стиле для обоих языков.
В случае printf vs std::cout разница точно такая же между select vs poll (epoll/kevent более продвинутые варианты, и они в данном случае для сравнения не годятся). Либо тоже самое, как делают некоторые студенты считаться данные из файла функцией read не блоками, а по-байтно склеивая все это в единый массив.
Конечно, в предложенном варианте std::cout будет быстрее.
Но в реальном коде вызовы выглядят не:
for (i = 0; i < n; i++) std::cout << a[i];
A вот так:
std::cout << "Привет " << name << "! Как у тебя дела? Что делал " << day << "? Какие планы на " << next_day ....
Код на C:
printf("Привет %s! Как у тебя дела? Что делал %s? Какие планы на %s? " .... "", name, day, next_day ...);
И в этом случае printf быстрее, что может стать "узким горлышком" для std::cout, когда их очень много. Правда теряешь на compile-time проверках типов (хотя современные компиляторы умеют проверять содержимое формат-строки), и опять же это не такая сильная проблема — код отлаживается один раз, запускается множество раз.
Даже boost::asio замечательно написанный, но сильно перегруженный постоянными перекладываниями из одного контейнера в другой дескрипторов сокетов.
Даже в его случае, его можно выкинуть, написать все на чистом API и получить лишние 10-20% производительности в реальном приложение, а не на "сферическом коне в вакууме", когда тесты гоняются на внутреннем интерфейсе машине, или в лучшем случае в локальной сети между двумя серверами.
Здравствуйте, andrew.f, Вы писали:
AF>Это такой забавный троллинг?
это ты так прикидываешься?
AF>Или Вы серьезно не умеете оптимизировать под разные типы системных вызовов?
где в моем примере использование системных вызовов? или ты таки считаешь, что printf() это системный вызов?
AF>И в этом случае printf быстрее, что может стать "узким горлышком" для std::cout, когда их очень много.
снова прикидываешься?
AF>Даже boost::asio замечательно написанный, но сильно перегруженный постоянными перекладываниями из одного контейнера в другой дескрипторов сокетов. AF>Даже в его случае, его можно выкинуть, написать все на чистом API и получить лишние 10-20% производительности в реальном приложение, а не на "сферическом коне в вакууме", когда тесты гоняются на внутреннем интерфейсе машине, или в лучшем случае в локальной сети между двумя серверами.
ни о чем
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, smeeld, Вы писали:
S>>И это компилируется в компактный простой ассемблерный листинг с jmp/jne/je.. скачками в пределах файла.
EP>Этот вариант даже медленнее чем вариант с goto.
Смотря что ты оптимизировал. Это лишь вариант ручного инлайнига сильно разветвленного кода, в отличии от типичной мешанины, когда когда обработка ошибок в одном месте с основным потоком кода.
В реалиях C++ код выглядит примерно также, у него точно также после каждого вызова есть проверка результата выполнения, и в случае ошибки — возврат на уровень выше вызовом throw. Тот самый прямой happy-path.
S>>В случае же использования С++ try/catch сплошные call XXXXXXX S>> в дебри либ и фреймворков, которые вызываются по ходу выполнения потока, постоянно прерывая его.
EP>Подробнее.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, andrew.f, Вы писали:
EP>>>Так мы же про общий случай, сортировка — это просто иллюстрация. AF>>qsort какой то неудачный пример — функция стандартная, и ей "сто лет в обед",
EP>Да какая разница? Вот например GLib: EP>
AF>>и есть масса других вариантов библиотек сортировки.
EP>Покажи хотя бы пример интерфейса сортировки на C, которая будет принимать любой предикат, любой тип последовательности, с любыми элементами.
Ты же сам прекрасно знаешь, что в этом случае код на макросах пишут.
Альтернативный вариант — в качестве объекта абстрации выбирается функция, но все объявления функций делаются через static inline.
Функция предикат, тоже пишется со static inline. И о чудо — при передаче в качестве параметра адреса static inline функции в другую static inline функцию — все это замечательно сворачиваeтся без лишних call XXXX.
Можешь сам попробовать — написать простой пример с подобными вызовами и посмотреть ассемблеровскую портянку, пропустив через gcc -S ....
Сорри, но из готовых алгоритмов, что у меня есть — код проприетарный, то есть закрытый.
Поищу открытые проекты с подобными наработками — поделюсь.
AF>>В контр-пример можно привести, что printf в большинстве случаев работает быстрее std::cout (можно глянуть на тесты boost::format — там это замечательно видно).
EP>Пример не равноценный — std::cout медленный потому что много чего делает, и имеет customization points. EP>А qsort медленный — потому что интерфейс такой
Вот в том то и дело, что сравниваются абсолютно неадекватные вещи, и при этом говорится один язык лучше второго, потому что вот в этом конкретном случае реализовано лучше.
Идеального варианта, подходящего под все случае жизни никто до сих пор не придумал.
EP>Покажи интерфейс сортировки, или например rbtree.
Я про это выше написал. Поищу вариант открытой библиотеки — скину.
AF>>И опять же под линуксом Firefox (всеми горячо любимым gcc) официально собирается с -Os, и он правда в такой варианте сборки работает быстрее, чем с -O2/-O3.
EP>Есть тесты?
Тесты на mozilla.org.
Самый простой вариант — качаем официальную сборку Firefox for Linux с mozilla.org и набираем в адресной строке about:buildconfig. Смотрим параметры сборки. Для 64-битных систем до сих пор 32 битные сборки, по банальной причине — памяти кушает больше, а на производительности никак не сказывается.
В дистрибутивах тоже самое — Firefox собран с -Os и без поддержки exceptions и rtti, но зато не (Chrome, LLVM тоже без исключений и rtti собираются, если что).
Например, у меня Debian:
Здравствуйте, andrew.f, Вы писали:
AF>Даже boost::asio замечательно написанный, но сильно перегруженный постоянными перекладываниями из одного контейнера в другой дескрипторов сокетов. AF>Даже в его случае, его можно выкинуть, написать все на чистом API и получить лишние 10-20% производительности в реальном приложение, а не на "сферическом коне в вакууме", когда тесты гоняются на внутреннем интерфейсе машине, или в лучшем случае в локальной сети между двумя серверами.
boost::asio и на тестах через внутренний интерфейс сливает релизации на чистом C API.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Во-первых у тебя memory-bounded вычисления — большую часть времени будет занимать пересылка данных в/из памяти. Чтобы обойти этот эффект, попробуй сделать num_samples = 64k (чтобы попасть хотя бы в L3), и добавить внешний цикл "repeat". EP>Во-вторых попробуй добавить volatile на i, чтобы отключить оптимизатор. EP>И в-третьих тут один цикл с несколькими проверками — тут branch predictor должен неплохо сработать. В реальном же коде, эти проверки будут размазаны тонким слоем по всему call graph, и не всегда будут вызываться в циклах. Плюс они забивают predictor table.
Посмотрел дизасм. Оказывается оптимизатор убирает все мои if-ы и векторизует цикл. Если ограничить размер массива и сделать внешний цикл, а в if-е сравнивать что-нибудь еще, что оптимизатор не может выбросить, векторизация не выполняется вообще. Если добавить volatile — все становится очень плохо, так как компилятор генерирует кучу загрузок и не может в векторизацию, независимо от налиция или отсутствия ветвлений.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Во-первых у тебя memory-bounded вычисления — большую часть времени будет занимать пересылка данных в/из памяти. Чтобы обойти этот эффект, попробуй сделать num_samples = 64k (чтобы попасть хотя бы в L3), и добавить внешний цикл "repeat". EP>Во-вторых попробуй добавить volatile на i, чтобы отключить оптимизатор. EP>И в-третьих тут один цикл с несколькими проверками — тут branch predictor должен неплохо сработать. В реальном же коде, эти проверки будут размазаны тонким слоем по всему call graph, и не всегда будут вызываться в циклах. Плюс они забивают predictor table.
Если выключить оптицизацию, код для циклов генерируется одинаковый, только во втором есть branch. Время работы тоже примерно одинаковое, первый цикл, без branch-а, может быть на 5-10% быстрее, но не больше. Это при том, что тело цикла это две загрузки и одно сохранение, в общем — все очень быстро и влезает в L2 кэш.
ну и зачем ты "это" тут оставил? разве я спрашивал как работает std::cout или printf() ?
если ты так и не понял мой посыл, то он состоял только в том — что мне плевать на потроха printf(), и в том, что std::cout имеет больше шансов на успешную оптимизацию и более типобезопасен.
но что я понял, так это то, что ты предлагаешь вместо std::cout юзать *надцать перегрузок рукоблудного printf(). но нет, спасибо, это не мой путь.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, andrew.f, Вы писали:
AF>По аналогии select vs poll. select — серия системных вызовов. poll — один системный вызов с большим массивом входных данных.
т.е. в твоей реальности, select() мониторит только один дескриптор? (страшно жить)
и да, плюс poll() не в этом.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, VladFein, Вы писали:
VF>Поменяйте местами вызовы тестов и результат будет противоположным. VF>Надо объяснить — почему?
ничего не изменилось. нужно объяснять, почему?
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
VF>>Поменяйте местами вызовы тестов и результат будет противоположным. VF>>Надо объяснить — почему? X>ничего не изменилось.
А новый результат можно посмотреть?
X>нужно объяснять, почему?
Потому, что только что прочитанные данные второй раз читаются быстрее.
Здравствуйте, niXman, Вы писали:
X>Здравствуйте, VladFein, Вы писали:
VF>>Поменяйте местами вызовы тестов и результат будет противоположным. VF>>Надо объяснить — почему? X>ничего не изменилось. нужно объяснять, почему?
Кстати, а зачем измерять время организации цикла и доступа к куче памяти?
Предлагаю другой тест:
три момента:
1. где мне взять вендус?
2. не очень понимаю, как работает этот тест, и что он тестирует. чем предыдущий тест плох? замеряет то, что не нужно? — так и должно быть, мы меряем штатное использование, а не синтетическое
3. ты и andrew.f, похоже совсем не в курсе, как работают стандартные потоки
так уж и быть, объясню: дело в том, что манипулятор std::endl выполняет бОльшую работу, которую символ '\n' используемый для printf() — не выполняет.
в моем тесте именно по этому нет ни первого, ни второго.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
Здравствуйте, niXman, Вы писали:
X>три момента: X>1. где мне взять вендус?
а чем на никсах меряют время когда не хотят 4 млн. циклов?
X>2. не очень понимаю, как работает этот тест, и что он тестирует. чем предыдущий тест плох? замеряет то, что не нужно? — так и должно быть, мы меряем штатное использование, а не синтетическое
Мой тест меряет время исполнения ОДНОГО вызова, в процессорных циклах. При чём здесь "синтетическое" использование? А "штатное", по-твоему, это замер всякого мусора?
X>3. ты и andrew.f, похоже совсем не в курсе, как работают стандартные потоки X> так уж и быть, объясню: дело в том, что манипулятор std::endl выполняет бОльшую работу, которую символ '\n' используемый для printf() — не выполняет. X> в моем тесте именно по этому нет ни первого, ни второго.
А я (не скажу за andrew.f) и не интересуюсь тем, КАК они работают.
После того, как я убрал std::endl (который в реальной жизни очень часто встречается), результат изменился не существенно:
1 3.333000 D:\Code\2010\ConsoleTest\Release\ConsoleTest.exeprintf: 176648
1 3.333 D:\Code\2010\ConsoleTest\Release\ConsoleTest.execout : 1816776
Press any key to continue . . .