LINUX.ORG.RU

Сетевой движок на Си. Вычитывание данных из сокета в 4кб буферы.

 


2

1

Хотелось построить сетевую подсистему, написанную на крестах, на такой идее: разные протоколы/модули обмениваются «цепочками буферов» (блохчейнами ога), в том числе и сетевая подсистема. Например, если кто-то хочет отправить по вебсокету 12 кб данных юзеру, то этот кто-то просит у центрального менеджера ресурсов 3 блока, наполняет их данными, чейнит их в одну цепь и кидает поинтер на первый блок в «сеть». Epoll-driven «сеть» отправит все данные (когда сможет) из этих блоков последовательно в сокет и отдаст «менеджеру ресурсов» блоки обратно по мере отправки. Блоки переюзываются всеми компонентами, потребляются по мере нужды, а новые коннекты не жрут память только по факту коннекта, тяжелее заддосить, всё красиво. Короче, общая идея в том, что есть некий slab-аллокатор блоков, владеющий ими и выдающий в аренду и блоки, протекая через все подсистемы и задерживаясь в сетевом модуле, если сразу запсать в сокет не удалось (больше чем TCP window или другой прикол), и в конце возвращаются в блокопрародитель обратно. Памяти при таком подходе в целом в любой момент сожрано меньше, чем если бы при наших 5к соединений каждый коннекшен бы держал собственный буфер на пару метров.

Одно но: если такая моя «сеть» решила вычитать из сокета мегабайт, то она сделает 1024/4 = 256 сисколлов read(), чтобы выжрать данные в цепь 4кб-блоков вместо одного сисколла, если бы у неё был буфер на 1 метр. На отправке те же приколы - рост числа сисколлов.

Да, есть оптимизации - можно юзать блоки разных размеров в разных местах, можно смотреть на Content-Length в случае протокола HTTP и готовиться к мегабайту оптимальнее заранее и т.п.

Но вот хотелось бы послушать разных историй про подобные «движки сети» где были похожие идеи и где авторы нашли оптимум.

Про recvmmsg/sendmmsg и io_uring в принципе в курсе.

UPDATE

«4 кб буферы» отошли в прошлое, текущее понимание затеи как «цепочка буферов разной фиксированной длины», например HTTP-ответ юзеру может быть цепочкой из буфера 4КБ (на хидеры) и за ним буфер в мегабайт под body.



Последнее исправление: kilokolyan (всего исправлений: 3)

мсье хочет переизобрести Streams из system V ? кстати там всё логично и красиво

или я просто не понял вопроса

MKuznetsov ★★★★★
()

Во всяких HTTP3 будет похожая херня over UDP тока

menangen ★★★★★
()

если бы у неё был буфер на 1 метр.

Нету протоколов нижнего уровня с целыми пакетами размером 1 метр

Harald ★★★★★
()
Ответ на: комментарий от Harald

Хочешь сказать, что даже с буфером 1 мб я сделаю неблокирующих read() (и столько же раз проснется epoll) столько же, сколько придет пакетов? Кажется нереальным. Ядро наверное может возвращаться из epoll сильно реже, чем падают irq от прихода пакетов…

kilokolyan
() автор топика

За один сисколл ты целый мегабайт вряд ли прочитаешь, если у тебя нормальная асинхронная система, просто потому что за раз так много не приходит из за ограничений протоколов нижнего уровня (см. MTU например), а для отправки можно использовать sendmmsg, который позволит сократить количество сисколлов.

Куда более серьезная проблема при таком подходе мне кажется это обработка данных. Например, далеко не каждая json библиотека имеет потоковый интерфейс вида std::optional<JsonObject> parser_update(JsonParserState& state, std::span<char> chunk);, который позволит по кускам данные обрабатывать. Если придется копировать в новый отдельный буфер данные для обработки то вся эта затея не стоит времени вообще.

Oxd34d10cc
()
Ответ на: комментарий от kilokolyan

Не, ну в теории, на гигабитных, десятигигабитных и выше соединениях может быть, там и драйвер может прерывания получать не на каждый пакет, а по расписанию или по достижению какого-то количества пакетов в буфере. Либо когда ты слишком редко read() дёргаешь.

Harald ★★★★★
()

Там разве биржевые торгаши уже не всё оптимизировали под завязку, есть разве потанцывал улучшения?

I-Love-Microsoft ★★★★★
()

Ничего не понял. Вы хотите, чтобы пользовательское приложение писало сразу в DMA память фрейма? А как тогда быть с безопасностью, ОС же не сможет контролировать заголовки? Если же безопасность и универсальность для вашей узкой задачи не важна, то берите DPDK и отправляйте данные им с максимально теоретической скоростью.

snizovtsev ★★★★★
()
Ответ на: комментарий от Harald

Нету протоколов нижнего уровня с целыми пакетами размером 1 метр

Есть segmentation offload в сетевых карточках, который позволяет нарезать большой буфер на кусочки в asic-ом.

snizovtsev ★★★★★
()

хотите ? делайте, в чем вопрос ?

просто почесать языком таких вас таких

anonymous
()

Есть huge page по 2мб и 1гб. Вместо кучи маленьких блоков можно выделить 1 большой

SR_team ★★★★★
()
Ответ на: комментарий от cdshines

А этот знаменитый «LMAX disruptor» - это разве не тупейший кольцевой буфер?

kilokolyan
() автор топика

Что-то здесь курсовой / дипломной работой попахивает… Не, не угадал? ;)

А если серьезно, то в контексте TCP и буферизации в user-space на чтение и запись идея использовать linked-list of fixed-sized buffers довольно ущербна, и в общем случае не жизнеспособна «я щитаю». Вы уж признайтесь чего Вы хотите добиться, а всезнающий all уж как-нибудь подскажет куда копать…

bugfixer ★★★★★
()
Ответ на: комментарий от snizovtsev

Он изобретает кернел байпас, вместо чтоб погуглить что уже сделано до него :) но дуинг ит вронг. Без железа уровня инфинибанды это все суходрочка с ботлнеками не там :)

slackwarrior ★★★★★
()
Ответ на: комментарий от slackwarrior

Он изобретает кернел байпас

Что-то мне подсказывает что человек ещё даже не приблизился к моменту когда kernel bypass становится полезным. Но ведь не колется на тему собственно задачи, а жаль - даже интересно стало ;)

bugfixer ★★★★★
()
Ответ на: комментарий от slackwarrior

Торгаши не долбятся в епол, а пекут себе асики и всяко игнорят ведерный TCP стек :)

Ну, далеко не всегда это оправдано - ситуации разные бывают…

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

А если серьезно, то в контексте TCP и буферизации в user-space на чтение и запись идея использовать linked-list of fixed-sized buffers довольно ущербна, и в общем случае не жизнеспособна «я щитаю».

А можно как-то это объяснить логически?

kilokolyan
() автор топика
Ответ на: комментарий от bugfixer

Вы уж признайтесь чего Вы хотите добиться, а всезнающий all уж как-нибудь подскажет куда копать…

Есть N модулей, которые иногда хотят что-то посылать через сеть или принимать из сети. Чаще всего послать они хотят что-то меньше чем 4 КБ, но иногда юзер спрашивает чё-то крупнее и надо отправить 256 КБ, скажем. Под модулями понимаются, например, объекты классов разных протоколов и объект, отвечающий за сеть, в котором крутится epoll.

Как сделано сейчас: по мотивам чтения исходников nginx: в модуле epoll триггернуло сколько-то сокетов. Модуль нашёл по этим сокетам указатели на объекты класса Connection и дёрнул им ptr->eventRead(). Объект заранее имеет какой-то выделенный буфер и сам вызывает ::read() на этот буфер и как-то дальше процессит свои данные. Чаще всего все модули дёргают некий центральный объект, отвечающий за бизнес-логику и говорят ему, типа «тебе по вебсокету вот упало по такому-то коннекту, позырь».

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

А когда модуль «бизнес-логики» пожелал через какой-то коннект отправить юзеру блок данных, то это тоже криво сделано: буквально выделил памяти любой кусок, заполнил чем-то и отдал коннекшену, а тот постепенно отправит и освободит память.

Хочется всё это унифицировать в плане управления памятью. В этой системе уже есть «менеджер ресурсов», представляющий собой набор тупейших slab-аллокаторов, откуда сдают в аренду и возвращают разные объекты разных классов. И вот хочется поручить этому менеджеру ресурсов ещё и заниматься блоками памяти. Возможно не фисированного (4, 8, 16, 32). Захотел что-то отправить по вебсткету - запросил у менеджера кучку блоков, заполнил их, выстроил в цепочку и кинул цепочку в «объект-connection-websocket», тот когда доотправляет - вернёт блоки тому же менеджеру.

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

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

А можно как-то это объяснить логически?

Можно. На чтение Вы хотите иметь достаточно большой буфер чтобы за один recv() с хорошей статистической вероятностью вычитывать всё что ядро успело у себя забуферизировать с момента последнего wake up, ну и начиная с какого-то размера - то что можно обработать сразу не вычитывая остальное. На запись Вы абсолютно точно хотите дергать за send() один раз на весь объем данных которые Вы хотите отправить прямо сейчас (иначе придётся смотреть в сторону TCP_CORK, это даже если игнорировать накладные расходы связанные с extra send() syscalls как таковыми). Учитывая современные реалии (размеры send/recv буферов на стороне ядра в мегабайты - десятки мегабайт) с fixed-sized chunks Вы будете существенное (если не всё) время проводить на поддержание iovec структур вместо того чтобы тратить CPU cycles на что-то более полезное (ну или индивидуальные блоки будут нереально большими и на 99% не использованными). Это исключительно с точки зрения оптимального общения с сокетами, и того что в результате будет происходить on-the-wire. Дальше - больше. Заморачиваться со своими аллокаторами нужно только после того как профайлер сказал что очень много времени проводится в malloc()/free(), и только после того как у Вас имеется вменяемая статистика снятая под «боевыми» нагрузками которая Вам позволит сделать что-то более эффективное. В общем, кажется мне что покаместь Вы собрались копать ну совсем в неправильном направлении… Я правильно понял что мы говорим о чём то уже существующем и работающем? Начните с профилирования прежде чем ввязываться в любые переделки. Наверное лучшее что можно посоветовать на данном этапе.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

По первой части про «один раз» сразу: есть «векторные» способы - readv/writev, например. То есть, syscall у тебя остаётся всё ещё один.

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

По первой части про «один раз» сразу: есть «векторные» способы - readv/writev, например. То есть, syscall у тебя остаётся всё ещё один.

Всё правильно. Которые принимают iovec который ещё нужно проинициализировать. Что с fixed-sized chunks мгновенно превращается в O(N) в лучшем случае, в худшем - O(N^2) (если на сокете затык и он принял не всё и надо повторять).

bugfixer ★★★★★
()
Ответ на: комментарий от kilokolyan

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

Ошибка в том что Вы передаёте ownership над этой памятью объекту который оборачивает / инкапсулирует сокет. Это неправильно по многим причинам (сокет должен знать как именно эта память аллоцирована etc). Подкидываю идею: нужен Connection::sendOrBuffer(const char* buf, size_t sz) который до тех пор пока на сокете затыка нет тупо форвардит в ::send(), и только если отправилось не всё добавляет неотправленное в свой private буфер. И здесь у Вас открываются горизонты для бесконечных оптимизаций как именно этот буфер мантейнить и что вообще с ним делать. И эту логику можно улучшать постепенно по мере возникновения необходимости абсолютно не влияя на клиентский код дергающий за sendOrBuffer(). И не благодарите ;)

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

А я считаю, что передать владение над буфером или их цепочкой - нормально, когда об этом явно сказано в интерфейсе. И смысл «менеджера ресурсов» в том, что все модули приходят вернуть в него то, что получили во владение от других зачем-то: например в том кейсе про send: «ой, я прям щас отправить не могу, забираю себе и потом освобожу сам». Забрать себе тупо указатель куда дешевле копирований в приватный буфер. Сетевой модуль вообще всегда забирает себе во владение всё переданное, а клиентский модуль считает, что передал владение и «там освободят», независимо от того, ушло все в ядро в этом вызове или нет. О том, когда это удастся запихать в сокет думает сетевой модуль. Я не знаю кейсов, когда клиенту сетевого модуля жаль отдать во владение какой-то свой буфер. Он ведь этот буфер специально и заводил, чтобы через него показать данные контрагенту - понадобится буферное место снова - возьмёт новый буфер у манагера ресурсов.

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

Забрать себе тупо указатель куда дешевле копирований в приватный буфер.

И да и нет. В ожидании того что сокет не congested не копировать вообще и тупо передать указатель ядру однозначно дешевле, и это именно тот use-case под который нужно оптимизироваться. А Вы стреляете себе в ногу заставляя клиентов аллоцировать память под сообщения строго определённым образом, всегда.

bugfixer ★★★★★
()
Ответ на: комментарий от kilokolyan

понадобится буферное место снова - возьмёт новый буфер у манагера ресурсов.

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

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Ядру и передаётся тупо указатель всегда. Или я не понял поинт.

Аллоцировать память как-то однообразно - норм, если мы желаем zero copy путём передачи собственности на буферы. Если собственность над чем-то ходит туда-сюда, то это что-то обязано удовлетворять какому-то одинаковому контракту и уже никак не может быть сырым {void*, size_t} точно. Это уже какая-то структурка, шаредпоинтер или подобное.

kilokolyan
() автор топика
Последнее исправление: kilokolyan (всего исправлений: 1)
Ответ на: комментарий от bugfixer

Ну не дорого. Один вызов метода вида «верни поинтер с конца массива поинтеров и уменьши размер массива на 1» так себе дороговизна. Побочная фича: возможность писать снова быстрее, чем сокето-инкапсулятор скопирует себе в приватный буфер в плохом сценарии, а хороший частый сценарий может выглядеть даже как: буфер не поменял собственника, оставь себе. Это если «дай птр из конца масства-стека» считаем дорогим.

kilokolyan
() автор топика
Последнее исправление: kilokolyan (всего исправлений: 2)
Ответ на: комментарий от kilokolyan

Ну не дорого. Один вызов метода вида «верни поинтер с конца массива поинтеров и уменьши размер массива на 1» так себе дороговизна.

Вы глубоко заблуждаетесь. Это бесконечно более дорого чем не делать ничего. И Вы вынуждены иметь дело с fixed-size blocks.

Если собственность над чем-то ходит туда-сюда

Вот это уже проблема - в этом контексте она не должна ходить вообще никуда.

то это что-то обязано удовлетворять какому-то одинаковому контракту и уже никак не может быть сырым {void*, size_t} точно. Это уже какая-то структурка, шаредпоинтера.

Ещё как может. И поверьте - я знаю о чём говорю. Более того - как только сокет заткнётся Вы вынуждены будете так или иначе начинать ассемблировать то что потом будете подсовывать ядру когда сокет снова станет готов на запись. И я не понимаю как queue из arbitrary N разрозненных сообщений лежащих хрен знает где будет лучше чем тот буфер который я подготовлю сам по мере поступления оных, даже если это означает копирование (однократное, если всё сделать правильно) собственно данных.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Вы глубоко заблуждаетесь. Это бесконечно более дорого чем не делать ничего. И Вы вынуждены иметь дело с fixed-size blocks.

Не особо верю в такую логику про дорого. Это ничего - это потенциальный copy, но главное что на фоне всякой бизнес-логики, где такты проца тратят на более адские вещи, это нанокопейка. 2. Фиксед блоки не проблема у меня, т.к. все посылки относительно фиксед и в 99% вписываются в 4 кб. Ну сделаю выбор вариантов при аллокации - 4, 8, 16, 64.

kilokolyan
() автор топика
Ответ на: комментарий от bugfixer

Вот это уже проблема - в этом контексте она не должна ходить вообще никуда.

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

kilokolyan
() автор топика
Ответ на: комментарий от bugfixer

знаю о чём говорю. Более того - как только сокет заткнётся Вы вынуждены будете так или иначе начинать ассемблировать то что потом будете подсовывать ядру когда сокет снова станет готов на запись. И я не понимаю как queue из arbitrary N разрозненных сообщений лежащих хрен знает где будет лучше чем тот буфер который я подготовлю сам по мере поступления оных, даже если это означает копирование (однократное, если всё сделать правильно) собственно данных.

Да, вот это поинт интересный

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

Да, вот это поинт интересный

И вот здесь мы приближаемся к самому интересному: как только Вы согласитесь / осознаете что концепция «передачи ownership над памятью дабы добиться zero copy» за которую Вы с таким упорством держитесь приносит Вам больше проблем чем их решает (включая вопросы производительности), и задумаетесь над тем чего можно достичь отказавшись от неё, Вы увидите что открываются горизонты для действительно важных оптимизаций. А именно: (1) возможность создания / форматирования сообщений прямо на стеке а не фиг знает где (стек почти всегда «hot» и в кеше CPU), (2) возможность держать заготовочки сообщений в которых мы будем менять пару / тройку полей in-place и отправлять вместо того чтобы каждый раз их создавать с нуля, итд. И это только пара вещей которые в принципе невозможно сделать в Вашем текущем дизайне. И это то что народ реально делает когда скорость действительно важна (в особо критичных случаях эта логика вообще уносится на ПЛИС aka FPGA, но это уже совсем другая история).

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Я ещё немного поспорю:

  1. Про стек. Чтобы он был горячим настолько, чтобы в нём были горячи 4 килобайта для вашей посылки, вы должны существенно дофига гулять по стеку вглубь. Т.е. перед вашим отправляющим кодом должны быть очень глубокие вызовы чего-то. Или вы должны постоянно много юзать стек вглудь, создавать какие-то массивы на стеке по 500 байт на каждом шагу. Возможно это происходит, ладно. Но что мешает такой же горячей быть какой-то странице в хипе, которую вы юзаете для отправок? Ничего. Для VM что стек, что какая-то страница - один хрен, просто какие-то страницы; в ядре нет ничего, что держало бы именно стек горячее другого куска памяти, который вы активно юзаете. Я гоню?

  2. Я уточняю идею со страницами: страница меняет собственность ТОЛЬКО если сокет «не всё может сейчас». Тогда горячая страница отходит ему в собственность, а посылальщик вынужден брать себе новую, холодную. Но это по цене то же самое, как если бы сокет начал копировать содержимое себе в private страницу, которая тоже холодная, т.к. туда давно не писали. Так же давно, как не выделяли новую холодную в моём кейсе, потому что заюзать эту новую холодную - равновероятно с «записать в private страницу неотправленное». Даже если сказать «у тебя private - одна на всех посылальщиков, поэтому она горячая» - нет, т.к. если в private пришлось наложить данных от всех посылальщиков, значит это private достаточно большое и полезло в свои холодные области, куда давно не лазило в силу редкости ситуации, когда затык на сокете такой злой. Хотя это моё утверждение неверно вот для какой ситуации: все посылальщики хотели послать достаточно мало (64 байта).

  3. Насчёт inplace: опять же, ничто не мешает инплейсить в выделенной странице, которой ты всегда владеешь, пока сокет у тебя её не попросил из-за затыка. Копировать «шаблон» себе в private область - это то же самое, что ты подаришь эту страницу и начнёшь писать новый шаблон заново ввиду отхода предыдущего…

Видимо в разговоре важно уточнять размеры посылок. Если все шлют десятки байтиков, тогда копировать в private осмысленнее, чем забирать всю страницу, в общем надо мерять…

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

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

там мьютекс-то будет какой-нить, у этого менеджера памяти? тредов много или целый один?

alysnix ★★★
()
Ответ на: комментарий от alysnix

там мьютекс-то будет какой-нить, у этого менеджера памяти? тредов много или целый один?

Не тема треда.

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan

Я ещё немного поспорю

Re: стек. (1) Как минимум верхушка будет в TLB вне зависимости от того что происходит с отдельно взятыми cachelines. (2) Выделение памяти на стеке это одна (!!) инструкция манипулирующая esp регистром. Быстрее нет ничего.

Re: копирование в private буфер. Думайте об этом так: сокет уже заткнулся (а это единственный сценарий в предложенном дизайне когда вообще приходится что-то копировать). Это даёт вам немного времени подготовить данные так чтобы они были в формате позволяющем наиболее эффективную отправку когда сокет будет готов на запись снова (включая аспекты связанные с локальностью данных итп). Вы же откладываете эту работу до последнего момента когда любое дополнительное время требующееся на обработку будет действительно критично. Никто так не делает.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Про стек - да. Во многих случаях так.

Про копирование. Замерял тут время работы ::read() внезапно.

Код получения текущего времени:

uint64_t curr_nano_mono() {
  timespec ts;
  if (!clock_gettime(CLOCK_MONOTONIC, &ts)) {
    return ts.tv_sec * 1000000000L + ts.tv_nsec;
  }
  return 0;
}

Мерял так:

  auto t1 = curr_nano_mono();
  int rd = ::read(socket, ptr, size);
  auto t2 = curr_nano_mono();
  printf("read time %lu ns\n", t2 - t1);

И получилось такое:

// браузер присылает нам по websocket короткие посылки с интервалом в секунду примерно
    1 read time 26576 ns
    2 read time 23573 ns
    3 read time 23166 ns
    4 read time 19196 ns
    5 read time 16056 ns
    6 read time 22784 ns
    7 read time 18044 ns
    8 read time 23853 ns
    9 read time 24285 ns
   10 read time 18270 ns
   11 // брайзер начал посылать посылки чуть длиннее и сильно часто: read стал возвращаться быстрее (видимо что-то прогрето)
   12 read time 7085 ns
   13 read time 3573 ns
   14 read time 3815 ns
   15 read time 4042 ns
   16 read time 3456 ns
   17 read time 3815 ns
   18 read time 6751 ns
   19 read time 12444 ns
   20 read time 9010 ns
   21 read time 23797 ns
   22 // браузер снова начал слать что-то мелкое раз в секунду (байт 16 payload) как в начале
   23 read time 18039 ns 
   24 read time 15884 ns
   25 read time 19076 ns
   26 read time 18059 ns
   27 read time 22291 ns
   28 read time 18965 ns
   29 read time 17606 ns
   30 read time 17034 ns
   31 read time 16870 ns
   32 read time 12785 ns
   33 read time 23127 ns
   34 read time 17811 ns
   35 read time 18065 ns
   36 read time 17959 ns

Потом померял сколько у меня выжимает memcpy() вот таким методом. Возможно метод дебилен:


#include <iostream>
#include <vector>
#include <string.h>

uint64_t curr_nano_mono() {
  timespec ts;
  if (!clock_gettime(CLOCK_MONOTONIC, &ts)) {
    return ts.tv_sec * 1000000000L + ts.tv_nsec;
  }
  return 0;
}

struct Measure {
  uint64_t start_{};
  const char *name_{};

  explicit Measure(const char *_name)
  : start_(curr_nano_mono())
  , name_(_name)
  {
  }

  uint64_t elapsed() {
    return curr_nano_mono() - start_;
  }
  ~Measure() {
    printf("%s: %lu ns\n", name_, elapsed());
  }
};


int main() {

  const auto BUFFS = 32;
  const auto BS = 1024 * 1024 * 8;
  std::vector<char*> vect(BUFFS, nullptr);

  srand(time(nullptr));

  {
    Measure me("alloc");
    for (auto &p : vect) {
      p = new char[BS];
    }
  }

  {
    Measure me("fill");
    for (auto &buff : vect) {
      for (char *p = buff, *end = buff + BS; p < end; ++p) {
        *p = reinterpret_cast<size_t>(p) % 256;
      }
    }
  }

  {
    Measure me("copy");
    const auto ITERS = 1024;
    size_t bytes = 0;
    // Copy some bytes from random "from" to random "to" with random offset in "from".
    for (auto iter = ITERS; iter >= 0; --iter) {
      auto from = 0;
      auto to = 0;
      // this loop makes sure we use different buffers
      while (from == to) {
        from = rand() % BUFFS;
        to = rand() % BUFFS;
      }

      // small random offset
      auto offset = rand() % (1024*16);
      bytes += (BS - offset);

      ::memcpy(vect[to], vect[from] + offset, BS - offset);
    }

    auto elapsed = me.elapsed();
    double seconds = static_cast<double>(elapsed) / 1000000000.0;
    double cost = ((1024.0 * 1024.0) / (double)bytes);

    printf("Memcpy %.2lf byte/sec, took %.2lf sec, %lu ns\n", bytes / seconds, seconds, elapsed);
    printf("1MB copy time cost: %.2lf ns\n", elapsed * cost);
  }

  return 0;
}

Собрать и запустить так: rm a.out; g++ -std=c++11 -O3 memcpy-measure.cpp; ./a.out

И на моём lenovo carbon X1 gen 7 получил такое:

alloc: 53937 ns
fill: 769249218 ns
Memcpy 10147233381.31 byte/sec, took 0.85 sec, 846528728 ns
1MB copy time cost: 103336.15 ns
copy: 846548979 ns

Железо ноута:

i7-8565U
Type: LPDDR3
Type Detail: Synchronous
Speed: 2133 MT/s

Отсюда 1MB copy time cost: 103336.15 ns и из времени syscall ::read() в районе 5-25 ns можно сказать, что syscall мне сильно дешевле, чем мегабайт памяти копировать. Т.е. если у меня в сокет не влез мегабайт, то мне проще подарить указатель на буфер сетевой подсистеме и забыть, чем копировать.

kilokolyan
() автор топика
Последнее исправление: kilokolyan (всего исправлений: 5)
Ответ на: комментарий от kilokolyan

вот таким методом. Возможно метод дебилен

Конечно, метод крив донельзя. read’ы короткие, а memcpy тест трэшит CPU cache по самые помидоры. read’ы не могут быть дешевле memcpy по определению. И раз мы буферизацию на запись обсуждаем - давайте уж send мерять. Ну, и заодно давайте march=native поставим. И с no-builtin-memcpy поиграемся - скорее всего на длинных копиях libc’шный вариант будет рвать то что gcc сгенерит.

Но это всё равно - бессмысленное сравнение яблок и апельсинов. Затык у Вас в другом месте будет.

То что Вы задумали делать в плане буферизации возможно имеет смысл только если это какой-то стриминг с хорошо приедсказуемым send rate. Если это что-то в духе веб-сервера когда запросы валятся случайно и требуют ответов разной длинны - то Вы порастёте.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Конечно, метод крив донельзя. read’ы короткие, а memcpy тест трэшит CPU cache по самые помидоры. read’ы не могут быть дешевле memcpy по определению.

Предположим, web-сервер. Ответ весит килобайт 30 - это продукт gzip неких 100кб html, составленного из 5 походов в базу. Пока я этот ответ сформирую, вызвав походы в базу, поспав в ожидании ответов от базы (пробуждаясь на обработку http-запросов из других сокетов и совершая аналогичную возню отправкой запросов в базу), потом вызову шаблонизатор, какие-то ещё алгоритмы расцветки кода на станице, прочую гзиповалку… я уже непонятно сколько раз проедусь по кешам, свичну контекст и т.п. При чём тут «по определению»…

kilokolyan
() автор топика
Последнее исправление: kilokolyan (всего исправлений: 2)
Ответ на: комментарий от kilokolyan

это продукт gzip неких 100кб html, составленного из 5 походов в базу

Вот это вот всё займет несопоставимо больше времени и CPU cycles чем гипотетически необходимый memcpy() в случае если send() в сторону клиента не примет всё сразу. Но я думаю что примет, особенно если Вы SO_SNDBUF подкрутите (но он там и так сотни килобайт будет, если памяти на машинке не в обрез). И я теперь ну совсем не понимаю зачем Вы хотите свой memory manager (и тем более работающий с fixed-size blocks), и чем он Вам здесь хотя бы теоретически может помочь. И почему Вы думаете что у Вас вот конкретно этот вот memcpy() дергающийся с пренебрежимо маленькой вероятностью вообще на performance profiles мелькать будет. Не туда Вы собираетесь копать, ой не туда…

При чём тут «по определению»…

Потому как что send() что recv() (в любой их форме) включают в себя копирование между kernel and user space, и делают много ещё чего.

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

Потому как что send() что recv() (в любой их форме) включают в себя копирование между kernel and user space, и делают много ещё чего.

А, ну да.

kilokolyan
() автор топика
Ответ на: комментарий от kilokolyan
uint64_t curr_nano_mono() {
  timespec ts;
  if (!clock_gettime(CLOCK_MONOTONIC, &ts)) {
    return ts.tv_sec * 1000000000L + ts.tv_nsec;
  }
  return 0;
}

а ничо, что клок монотоник - это время работы всей системы, а не вашего треда? таки надо поставить - CLOCK_THREAD_CPUTIME_ID.

alysnix ★★★
()
Ответ на: комментарий от alysnix

а ничо, что клок монотоник

УмнО. Да что ж вы как чёрт из табакерки вылазите то всё время? Брысь.

ПыСы: как там очереди и треды?

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

клоки монотоники используют для расчета интервалов в абсолютном времени, например для таймаутов и слипов.

а вот .._CPUTIME_ID, для тредов и процессов, как раз для замеров производительности.

и пааапрашу впредь не путать!

alysnix ★★★
()
Ответ на: комментарий от alysnix

клоки монотоники используют для расчета интервалов в абсолютном времени, например для таймаутов и слипов.

И это именно то что интересует простых смертных живущих в реальном мире.

а вот .._CPUTIME_ID, для тредов и процессов, как раз для замеров производительности.

Кому надо, и TSC не поленятся вычитать.

и пааапрашу впредь не путать!

Конечно, конечно. Только какое отношение это к проблеме ТС имеет?

bugfixer ★★★★★
()
Ответ на: комментарий от bugfixer

И это именно то что интересует простых смертных живущих в реальном мире.

когда меряют производительность куска кода, не стоит одновременно измерять производительность всех активных кодов на вашем компе в данный момент. это ненаучно.

alysnix ★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.