Пе­ремен­ные и типы хороши, пока мы находим­ся внут­ри логики прог­раммы C++. Одна­ко рано или поз­дно ста­новит­ся нуж­но переда­вать информа­цию меж­ду прог­рамма­ми, меж­ду сер­верами или даже прос­то показать типы и зна­чения перемен­ных челове­ку разум­ному. В этом слу­чае нам при­ходит­ся зак­лючать сдел­ку со злоб­ным Сери­али­зато­ром и рас­пла­чивать­ся про­изво­дитель­ностью сво­его кода. В пос­ледней лек­ции Ака­демии C++ мы наконец дош­ли до глав­ного бос­са, которо­го нуж­но научить­ся побеж­дать с минималь­ными потеря­ми в ско­рос­ти выпол­нения кода. Поеха­ли!
 

Давным-давно...

В ста­родав­ние вре­мена муд­рые отцы‑осно­вате­ли прог­рамми­рова­ния научи­ли пос­ледова­тель­нос­ти бай­тов прев­ращать­ся в чис­ла, в логичес­кие усло­вия, в пос­ледова­тель­ность дей­ствий и даже в объ­екты реаль­ного мира. В каж­дом язы­ке было мно­го сущ­ностей, но стро­ились они из бай­тов совер­шенно по‑раз­ному. Порой пов­торить на одном язы­ке то, что в дру­гом было из короб­ки, — задача для силь­ных духом. Куда про­ще передать объ­ект клас­са в метод, написан­ный на дру­гом, более прис­пособ­ленном для это­го язы­ке. Но что делать, если нуж­ный метод находит­ся на уда­лен­ной машине или выпол­няет­ся в отдель­ном про­цес­се? Нуж­но прев­ратить нашу логику в бай­ты и передать при­нима­ющей сто­роне, готовой выпол­нить нуж­ный метод. Так родилась идея сери­али­зации — еди­ное пред­став­ление в бай­тах дан­ных и логики прог­рам­мно­го кода, понят­ное как на сто­роне отпра­вите­ля, так и на сто­роне получа­теля.

В этот момент начал­ся хаос. Были попыт­ки пос­тро­ить один Единс­твен­но вер­ный эта­лон­ный механизм сери­али­зации. Ста­ли появ­лять­ся и мно­жить­ся несов­мести­мые меж­ду собой про­токо­лы переда­чи дан­ных. Естес­твен­но, на это были свои при­чины: они слу­жили раз­ным целям и были опти­мизи­рова­ны для обра­бот­ки раз­ных дан­ных. Спер­ва вол­на пош­ла в мире Java, поз­же к праз­дни­ку жиз­ни при­соеди­нил­ся C#, искусс­твен­но огра­ничен­ный при рож­дении плат­формой от Microsoft (к счастью, зло час­тично повер­жено, славь­ся, C# на всех плат­формах). Были и наив­ные попыт­ки веб‑раз­работ­чиков: от неук­люжего PHP до все более популяр­ного JavaScript на сер­вере, на кли­енте и в тво­ей кофевар­ке. Во всех слу­чаях попыт­ки объ­явить себя единс­твен­но вер­ным путем в раз­работ­ке были обре­чены. Слиш­ком уж инди­виду­ален каж­дый раз­работ­чик и каж­дая реша­емая задача, а мно­гооб­разие язы­ков и их внут­ренних типов не поз­воля­ет переда­вать дан­ные без потерь меж­ду дву­мя диамет­раль­но про­тиво­полож­ными по сво­ей сути язы­ками или тех­нологи­ями. Во все вре­мена язы­ки и плат­формы объ­еди­няло, пожалуй, толь­ко одно: поч­ти все они были написа­ны на C/C++.

По­пыт­ки охва­тить все оче­ред­ным уни­вер­саль­ным язы­ком или тех­нологи­ей раз­работ­ки будут пред­при­нимать­ся еще не раз. Но наибо­лее муд­рые раз­работ­чики дав­но уже научи­лись догова­ривать­ся меж­ду собой, как упа­ковать бай­ты сущ­ности так, что­бы при получе­нии мож­но было рас­паковать в ана­логич­ную сущ­ность на сто­роне получа­теля. Глав­ное, пом­нить, что на берегу бай­товых потоков тебя ждет всег­да одно и то же — ненасыт­ный паром­щик Сери­али­затор. Раз за разом он будет поедать вре­мя выпол­нения тво­ей прог­раммы, такт за так­том, а вза­мен выдавать закоди­рован­ный текст в XML-фор­мате вмес­то объ­екта кон­фигура­ции прог­раммы или, нап­ример, пос­ледова­тель­ность бай­тов сог­ласно ASN.1 вмес­то струк­туры катало­гов фай­ловой сис­темы. И чем слож­нее его задача — тем доль­ше он будет ее выпол­нять, отни­мая дра­гоцен­ное вре­мя и сни­жая про­изво­дитель­ность при­ложе­ния...

 

Сколько сериализацию ни корми

Во­обще говоря, страш­ных вра­гов у про­изво­дитель­нос­ти нашего при­ложе­ния обыч­но три:

  1. Бес­кон­троль­ное копиро­вание объ­ектов и подобъ­ектов.
  2. Не­оправдан­ное динами­чес­кое выделе­ние памяти на куче.
  3. Без­думное и неэф­фектив­ное исполь­зование сери­али­зации.

C пер­выми дву­мя мы эффектив­но воева­ли в пре­дыду­щих уро­ках — вред их явный и бес­спор­ный, а потому основная борь­ба ведет­ся с ними, и, как пра­вило, успешно.

Но опас­нее всех наш тре­тий враг. Казалось бы, что такого в том, что­бы пре­обра­зовать нес­коль­ко целых чисел в стро­ку и передать по сети? В этот момент прог­раммист обыч­но забыва­ет о слож­ности пре­обра­зова­ния, хра­нения и переда­чи боль­ших мас­сивов бай­тов стро­ки, вмес­то нес­коль­ких лег­ковес­ных бай­тов чис­ла, что были у него изна­чаль­но.

Се­риали­затор — наш самый страш­ный враг. Без него нам не обой­тись, а он все шеп­чет о том, что если все оста­вить стро­ками, то мы получим ана­лог динами­чес­кой типиза­ции. «И нет смыс­ла тре­пыхать­ся, — говорит он, — ведь на сто­роне кли­ента от нас ждут стро­ку JSON или XML. Зачем же нам целое чис­ло, хра­ни в клас­се набор строк!» Страш­ные вещи тво­рят нес­час­тные раз­работ­чики, порабо­щен­ные таинс­твен­ным шепотом, пов­сюду в их коде стро­ки. Те же, что силь­нее духом, но не окрепшие разумом, нап­ротив, пре­обра­зуют заз­ря туда‑сюда дан­ные в бай­ты и обратно. Радос­тно потира­ет руки Сери­али­затор, видя, как стра­дает эффектив­ность. И сегод­ня мы вмес­те с тобой победим это зло!

 

Как закалялся код

Что­бы укро­тить Сери­али­затор, сле­дует сна­чала изу­чить его сла­бые сто­роны. Для это­го нам, во‑пер­вых, пот­ребу­ются навыки, получен­ные в пре­дыду­щих уров­нях:

  • уме­ние обра­щать­ся со стро­ками и бай­тами;
  • по­нима­ние сути динами­чес­кой типиза­ции;
  • оп­тимиза­ция кода при соз­дании и копиро­вании объ­ектов;
  • пред­став­ление вещес­твен­ных чисел в бинар­ном виде;
  • все осталь­ное из пре­дыду­щих уро­ков.

Во‑вто­рых, как мы пом­ним, стро­ки при переда­че дан­ных ста­новят­ся обыч­ным набором бай­тов, поэто­му любой про­токол, что тек­сто­вый, что бинар­ный, опе­риру­ет, по сути, бай­тами. Одна­ко тек­сто­вый, как пра­вило, дол­жен быть «челове­кочи­таемым», что озна­чает допол­нитель­ную работу Сери­али­зато­ру при пред­став­лении ска­ляр­ных зна­чений в бай­товом экви­вален­те. Ведь для того, что­бы прос­то прев­ратить целое чис­ло –123 в бай­ты стро­ки с десятич­ным пред­став­лени­ем чис­ла –123, нуж­но выпол­нить не такую уж и прос­тую опе­рацию. Для подоб­ного пре­обра­зова­ния в самом C/C++ не пре­дус­мотре­но сов­сем ничего, да и стан­дар­тная биб­лиоте­ка не раду­ет сво­им набором:

  • sprintf — поз­воля­ет не прос­то пре­обра­зовать чис­ло в стро­ку, но и соз­дать фор­матиро­ван­ное сооб­щение, что нам пока не нуж­но;
  • itoa — дела­ет ров­но то, что нам нуж­но: integer-to-array-of-characters;
  • stringstream — очень удо­бен для соз­дания чита­емо­го кода, но и мак­сималь­но питате­лен для Сери­али­зато­ра :).

Так­же есть биб­лиоте­ка Boost, чей lexical_cast еще менее пред­назна­чен для эффектив­ного пре­обра­зова­ния чис­ла в стро­ку.

Но для начала давай сами соз­дадим мак­сималь­но прос­той велоси­пед, который решит нашу задачу — пред­ста­вит целое чис­ло в десятич­ном виде, и погоня­емся напере­гон­ки с биб­лиотеч­ными фун­кци­ями. Код нашей фун­кции будет мак­сималь­но прост, никако­го ассем­бле­ра:

size_t decstr(char* output, size_t maxlen, int value)
{
if (!output || !maxlen)
return 0;
char* tail = output;
// Разбираемся со знаком
if (value < 0)
{
*tail++ = '-';
value = -value;
}
// Строим строку наоборот
size_t len = 0;
for (; len < maxlen; ++len)
{
*tail++ = value % 10 + '0';
if (!(value /= 10)) break;
}
// Поместилось ли
if (value) return 0;
// Завершаем строку
if (len < maxlen) *tail = '0';
// Разворачиваем строку
char *head = output;
if (*head == '-') ++head; // Пропускаем знак
for (--tail; head < tail; ++head, --tail)
{
char value = *head;
*head = *tail;
*tail = value;
}
// Возвращаем длину строки
return len;
}

Те­перь смот­рим, нас­коль­ко наша пря­моли­ней­ная реали­зация оправда­ла зат­рачен­ное на нее вре­мя по срав­нению с биб­лиотеч­ными фун­кци­ями (100 мил­лионов ите­раций в секун­дах, про­цес­сор i5-2410M, RAM DDR3 4GB, Windows 8.1 x64):

  • decstr: ~3,5;
  • snprintf: ~26;
  • itoa: ~6,2.

В тес­тах с вклю­чен­ной опти­миза­цией мы обго­няем стан­дар­тные биб­лиотеч­ные фун­кции с соот­ношени­ем ~1 : 8 : 2.

Про­исхо­дит это нес­лучай­но. Дело в том, что алго­ритм itoa завязан на базу исчисле­ния, — не всег­да 10, час­то так­же тре­бует­ся 16, и не толь­ко. Что до snprintf, то эта фун­кция вооб­ще не зна­ет, что пре­обра­зует имен­но чис­ло, так как спер­ва ей при­ходит­ся разоб­рать фор­мат. Наша же фун­кция не дела­ет прак­тичес­ки ничего лиш­него.

Ра­зуме­ется, это не зна­чит, что нам теперь во что бы то ни ста­ло нуж­но вез­де соз­давать свою фун­кцию, но сам по себе при­мер показа­тель­ный, ведь в мес­те мас­совой сери­али­зации зна­чений, пре­обра­зова­ний может тре­бовать­ся гораз­до боль­ше 100 мил­лионов в секун­ду, и, даже если рас­поточить по ядрам, мы име­ем все шан­сы не успеть при выборе некор­рек­тно­го алго­рит­ма пре­обра­зова­ния чисел в стро­ку.

Не­дооцен­ка такой базовой зат­раты ресур­сов, как сери­али­зация, может аук­нуть­ся серь­езны­ми зат­ратами на новую пар­тию сер­веров (ста­рина Сери­али­затор хитер и навер­няка сот­рудни­чает с про­дав­цами сер­верно­го железа :)). Кажет­ся, что подоб­ные вещи лег­ко находят­ся про­фили­ров­щиком. На деле же выходит, что логика сери­али­зации дан­ных очень тон­ким сло­ем раз­мазыва­ется по тво­ему при­ложе­нию и хорошо прик­рыва­ется личиной вызова биб­лиотеч­ных фун­кций.

 

Погружение в поток байтов

Воз­вра­щение сущ­ностей прог­рам­мной логики обратно в пер­воздан­ную при­роду бай­товых пос­ледова­тель­нос­тей никог­да не быва­ет прос­тым. Но для бинар­ных про­токо­лов прев­ращение целых чисел в бай­ты, как пра­вило, сво­дит­ся к побай­товой переда­че чис­ла, при­чем при­нято пра­вило, что стар­ший байт чис­ла переда­ется пер­вым. Дру­гими сло­вами, это озна­чает, что на сто­роне при­емни­ка на подав­ляющем боль­шинс­тве машин, где архи­тек­тура пред­став­ления целых чисел идет начиная от млад­шего бай­та, мы не смо­жем прос­то взять четыре или восемь получен­ных байт и при­вес­ти их соот­ветс­твен­но к int32_t или int64_t прос­тым при­веде­нием типа ука­зате­ля по сме­щению, наподо­бие *reinterpret_cast<int32_t*>(value_pointer).

Что­бы ста­ло понят­нее, про­иллюс­три­рую, в каком виде переда­ется 32-раз­рядное целое чис­ло со зна­ком с машины с x86-архи­тек­турой в боль­шинс­тво бинар­ных про­токо­лов для переда­чи по сети. Само чис­ло, нап­ример –123456789, на уров­не обра­бот­ки про­цес­сором и, соот­ветс­твен­но, в логике прог­раммы будет выг­лядеть так:

0 | 1 | 2 | 3 |
0xF8 | 0xA4 | 0x32 | 0xEB |

В то вре­мя как по сети это зна­чение передас­тся в обратном поряд­ке:

0 | 1 | 2 | 3 |
0xEB | 0x32 | 0xA4 | 0xF8 |

Ес­ли мы прос­то возь­мем мас­сив из бай­тов и попыта­емся получить из него чис­ло, интер­пре­тиро­вав наши четыре бай­та как 32-раз­рядное целое, у нас на выходе обра­зует­ся сов­сем дру­гое чис­ло: –349002504. Обще­го у них, как пра­вило, ничего нет. Для того же, что­бы получить исходное чис­ло, нуж­но либо к получен­ному зна­чению при­менить фун­кцию ntohl — net-to-host-long-integer, либо прос­то соб­рать по ука­зате­лю на зна­чение в бай­товом мас­сиве из четырех пос­леду­ющих байт нуж­ное целое:

inline int32_t splice_bytes(uint8_t const* data_ptr)
{
return (data_ptr[0] << 24) |
(data_ptr[1] << 16) |
(data_ptr[2] << 8) |
data_ptr[3];
}

Эта фун­кция будет чуть дешев­ле и эффектив­нее, чем собирать спер­ва ненуж­ный int32_t в ненуж­ном поряд­ке, что­бы потом перевер­нуть, как надо, его бай­ты. Кро­ме все­го про­чего, эта фун­кция будет работать под любой ARM-плат­формой, а вот попыт­ка разыме­новы­вать под некото­рые ARM-плат­формы бай­ты как int32_t со сме­щени­ем, не крат­ным четырем, при­ведет к завер­шению про­цес­са. В общем, при десери­али­зации луч­ше не делать ничего лиш­него.

Те­перь вкрат­це, почему поч­ти в любом бинар­ном про­токо­ле пер­вым по сети переда­ется стар­ший байт. Как пра­вило, по сети в пер­вую оче­редь переда­ют наибо­лее важ­ную информа­цию. Спер­ва заголо­вок с мета­информа­цией, затем уже сами дан­ные. Сна­чала в передан­ных дан­ных идет информа­ция о том, что в сле­дующих бай­тах, прос­то потому, что эти дан­ные нуж­но будет кор­рек­тно счи­тать. Ведь изна­чаль­но неиз­вес­тно, что счи­тывать, сколь­ко и как. Но если прис­лать в пер­вую оче­редь под­робную инс­трук­цию о том, что идет далее, то счи­тыва­ние прой­дет глад­ко. Так­же, эко­номя на дра­гоцен­ных бай­тах тра­фика, чаще все­го мета­информа­цию об отдель­ных чис­лах сра­щива­ют с самим чис­лом, встра­ивая ее в наиме­нее вос­тре­бован­ные стар­шие биты чис­ла таким обра­зом, что­бы потом мож­но было по мас­ке выцепить и само чис­ло «как есть», и важ­ные биты с мета­информа­цией.

Прос­той при­мер мы раз­бирали в уро­ке про кодиров­ки. В кодиров­ке тек­ста UTF-8 пер­вым бай­том переда­ется мета­информа­ция о том, сколь­ко бай­тов нуж­но счи­тать для получе­ния закоди­рован­ного сим­вола. Для кирил­лицы, исполь­зующей два бай­та под код в таб­лице Юни­кода, пер­вые три бита пер­вого бай­та всег­да 110, а затем уже идет кусок кода сим­вола.

Так же и с целыми чис­лами — стар­шие бай­ты их в без­зна­ковых целых поч­ти не исполь­зуют­ся и прек­расно под­ходят для переда­чи мета­информа­ции, для которой зачас­тую хва­тает нес­коль­ких битов, нап­ример раз­рядность целого: 1, 2, 4 или 8 под кодиров­ку чис­ла. Про­ще все­го, получив пер­вый байт, понять, сколь­ко бай­тов тре­бует­ся для счи­тыва­ния чис­ла, потом счи­тать бай­ты как целое и побито­во сде­лать AND с мас­кой без битов мета­информа­ции, получит­ся нуж­ное чис­ло за минимум опе­раций.

Для сим­воль­ных дан­ных внут­ри бинар­ного пакета текст кодиру­ется в бай­ты сог­ласно одной из общепри­нятых кодиро­вок. Пре­обра­зова­ния бай­тов в стро­ки и обратно мы уже про­ходи­ли.

Би­нар­ные про­токо­лы с фик­сирован­ным фор­матом, как пра­вило, про­ще все­го сери­али­зовать под­готов­ленны­ми струк­турами на эта­пе ком­пиляции. Прос­то при­вес­ти ука­затель на дан­ные к ука­зате­лю на струк­туру очень дешево, но нуж­но учи­тывать упа­ков­ку полей струк­туры либо хра­нить в струк­туре ука­зате­ли по сме­щению на дан­ные в передан­ном пакете, пре­обра­зуя их в поля лениво по зап­росу.

На порядок слож­нее раз­бирать струк­туры, которые фор­миру­ются динами­чес­ки. Здесь прос­транс­тва для манев­ра мень­ше и в помощь нам лишь про­токол и усло­вия реша­емой нами задачи. Нап­ример, для пакета дан­ных в фор­мате ASN.1 нуж­но спер­ва про­читать раз­мер заголов­ка с опи­сани­ем полей дан­ных, их раз­мера и сме­щения, разоб­рать заголо­вок, пос­ле чего нас­танет пора раз­бирать уже сами поля по сме­щению, которое мы изна­чаль­но не зна­ем, так как информа­ция об этом при­ходит на эта­пе запол­нения дан­ных.

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

Что­бы ста­ло чуточ­ку понят­нее, давай­те раз­берем при­мер. Пусть к нам при­ходит информа­ция о пла­теже в фор­мате:

  • иден­тифика­тор пла­тежа (UUID 16 байт);
  • сум­ма пла­тежа (32-раз­рядное без­зна­ковое целое);
  • опи­сание (null-terminated стро­ка в UTF-8);
  • ад­рес опла­ты (null-terminated стро­ка в UTF-8).

Пос­коль­ку поля 3 и 4, ско­рее все­го, про­изволь­ной дли­ны, мы не можем прос­то взять и получить адрес чет­верто­го поля, для нас это слиш­ком дорого. Если роль логики нашего при­ложе­ния — ВСЕГ­ДА счи­тывать и обра­баты­вать эти поля, мож­но сра­зу вычис­лить мет­ку начала чет­верто­го поля и ссы­лать­ся на него как на обыч­ную сиш­ную стро­ку. Ни в коем слу­чае не выс­читыва­ем зна­чения в std::string без осо­бой на то необ­ходимос­ти — это, во‑пер­вых, поч­ти навер­няка динами­чес­кое выделе­ние памяти под еще одну стро­ку, а во вто­рых, ненуж­ное копиро­вание дан­ных, которые уже пред­став­лены в виде стро­ки, закоди­рован­ной в UTF-8. Эко­номить на раз­боре сум­мы пла­тежа не име­ет смыс­ла, здесь нам в помощь наша фун­кция splice_bytes, а вот иден­тифика­тор пла­тежа мож­но интер­пре­тиро­вать как UUID, прос­то сос­лавшись на пер­вые бай­ты в начале пакета. На самом деле если и целое чис­ло при­сылать не в обыч­ном для сети фор­мате начиная со стар­шего бай­та, а в виде, обыч­ном для сер­верной логики, то мы получим не прос­то пакет дан­ных, а впол­не рабочий набор дан­ных с ука­зате­лями на зна­чения, готовые к работе в C/C++.

Те­перь пару слов о вещес­твен­ных чис­лах. На самом деле типы C/C++ float и double по раз­меру не отли­чают­ся от uint32_t и uint64_t и кодиру­ются сог­ласно стан­дарту IEEE 754 (вспо­мина­ем урок «Все, точ­ка, прип­лыли!»). Как пра­вило, в бинар­ных про­токо­лах вещес­твен­ные чис­ла с пла­вающей точ­кой еди­нич­ной и двой­ной точ­ности переда­ют и обра­баты­вают так же, как и целые чис­ла соот­ветс­тву­ющей раз­ряднос­ти, прос­то абс­тра­гиру­ясь от напол­нения. Ведь для битов в бай­те неваж­но, что они зна­чат, целое ли или вещес­твен­ное чис­ло с пла­вающей точ­кой.

Нес­коль­ко реже встре­чает­ся пред­став­ление в виде чис­лителя и зна­мена­теля, а, нап­ример, вре­мя в про­токо­ле NTP переда­ется с секун­дами с чис­лителем и фик­сирован­ным зна­мена­телем. Тем не менее вне зависи­мос­ти от пред­став­ления вещес­твен­ного чис­ла в про­токо­ле неиз­менно толь­ко то, что обра­бот­ки имен­но вещес­твен­ных чисел с пла­вающей точ­кой при переда­че дан­ных в про­токо­лах ста­рают­ся избе­жать. Прос­то потому, что вычис­ление вещес­твен­ного чис­ла дает пог­решность, которая отли­чает­ся на при­емни­ке и отпра­вите­ле, а переда­ча чис­ла как есть, как пра­вило, свя­зана раз­ве что с тем, что это чис­ло прос­то изна­чаль­но бы­ло, нап­ример хра­нилось в базе дан­ных как поле с вещес­твен­ным зна­чени­ем.
Би­нар­ные про­токо­лы вво­дят­ся имен­но для того, что­бы опти­мизи­ровать сери­али­зацию дан­ных, и из спе­цифи­кации уже оче­вид­но, как эффектив­но пос­тро­ить алго­ритм раз­бора. Куда более при­быль­ны для Сери­али­зато­ра столь любимые прос­тыми смер­тны­ми челове­кочи­таемые про­токо­лы.

 

Жатва сериализации

По­гово­рим о XML, JSON, YAML, где чис­ла ста­новят­ся стро­ками, а бай­товые пос­ледова­тель­нос­ти допол­нитель­но экра­ниру­ются, что­бы их мож­но было передать как стро­ки. Что может быть зат­ратнее попыт­ки закоди­ровать даже килобай­тный файл в стро­ку JSON с помощью, нап­ример, Base64? Даже прос­тое экра­ниро­вание кавычек в обыч­ных стро­ках, переда­ваемых в JSON, уже недеше­вая опе­рация, и при десери­али­зации, разуме­ется, пот­ребу­ется обратная опе­рация. То же каса­ется и экра­ниро­вания XML-тегов в стро­ках с угло­выми скоб­ками в том же SOAP. Здесь Сери­али­затор влас­тву­ет без­раздель­но и собира­ет такую жат­ву, о которой в бинар­ных про­токо­лах даже и не меч­тал.

warning

Ес­ли дорожишь эффектив­ностью при переда­че дан­ных, то отда­вай пред­почте­ние бинар­ным про­токо­лам (если, конеч­но, у тебя есть выбор).

Здесь, в тек­сто­вых про­токо­лах, потери чудовищ­ны и неиз­бежны. Единс­твен­ный спо­соб их хоть как‑то сни­зить — это при­дер­живать­ся ряда прос­тых пра­вил. Их все­го десять, прос­тыми сло­вами они зву­чат так:

  1. Ми­ними­зируй количес­тво пре­обра­зова­ний меж­ду сери­али­зован­ным и десери­али­зован­ным зна­чени­ем. В иде­але мы дол­жны один раз про­читать либо один раз записать одно зна­чение, и то по тре­бова­нию, толь­ко в момент надоб­ности это­го поля. Если логика обра­бот­ки дан­ных у нас сквоз­ная и мы реали­зуем некий механизм дооб­работ­ки дан­ных пакета перед переда­чей его даль­ше, то есть смысл сох­ранить исходный пакет дан­ных, передав его же даль­ше с необ­ходимы­ми изме­нени­ями, и так миними­зиро­вать зат­раты на про­межу­точ­ную сери­али­зацию/десери­али­зацию.
  2. Не пло­ди вез­де и всю­ду стро­ки под пред­логом, что весь пакет дан­ных в JSON или XML по сути одна боль­шая стро­ка. Дан­ные поч­ти всег­да при­ходят типизи­рован­ными, и тип им дан не прос­то так. Не так уж и удоб­но обра­баты­вать рост/воз­раст/вес/сум­му в виде стро­ки. Осо­бен­но учи­тывая то, что для хра­нения стро­ки поч­ти навер­няка исполь­зован кон­тей­нер std::string/std::wstring и это при­вело к копиро­ванию дан­ных стро­ково­го пред­став­ления чис­ла и навер­няка к выделе­нию дан­ных на куче, вмес­то того что­бы при­вес­ти к целому чис­лу, или UUID, или логичес­кому зна­чению true/false.
  3. Оп­тимизи­руй по мак­симуму про­цесс экра­ниро­вания строк, пре­обра­зова­ния целых и вещес­твен­ных чисел и логичес­ких кон­стант в стро­ку и обратно. Вооб­ще, про­цесс сери­али­зации и десери­али­зации дол­жен быть тем мес­том в коде, в котором ты дол­жен быть уве­рен. Ты дол­жен знать, что уж тут‑то не тра­тит­ся ни одно­го лиш­него кван­та вре­мени ни на одно пре­обра­зова­ние. Ну не нуж­но для замены \" на " в стро­ке реали­зовы­вать алго­ритм кубичес­кой слож­ности! Так­же сто­ит миними­зиро­вать соз­дание про­межу­точ­ных объ­ектов типа std::string для хра­нения вре­мен­ных резуль­татов, впол­не дос­таточ­но ука­зате­лей на исходную стро­ку и стро­ку с выводи­мым резуль­татом.
  4. Убей в себе желание исполь­зовать std::stringstream. Пом­ни о том, что в ито­ге при­дет­ся делать str() или бегать ите­рато­ром еще раз по все­му, что насоби­ралось. Это не говоря о сег­менти­рован­ности памяти пос­ле активно­го исполь­зования std::stringstream во всех мес­тах, где нуж­на сери­али­зация!
  5. Еще раз: при­ори­тет ука­зате­лей на сим­волы в стро­ке перед вся­чес­кими про­межу­точ­ными std::string с вре­мен­ными резуль­татами!
  6. Ес­ли исполь­зуешь фун­кции Boost, замеряй вре­мя выпол­нения их работы в срав­нении с прос­тей­шими велоси­педа­ми. Если ока­зыва­ется, что фун­кции Boost работа­ют в 35 раз мед­леннее пря­мого под­хода, не дела­юще­го ничего лиш­него, зна­чит, исполь­зуют­ся они зря!
  7. Не бой­ся страш­ного кода, если на кону эффектив­ность выпол­нения самого узко­го учас­тка кода. Пусть у тебя будет switch в две стра­ницы кода, который выпол­няет работу 100 мил­лионов ите­раций за две секун­ды, чем полимор­физм с кучей visitor’ов и огромным сте­ком вызовов, выпол­няющих ту же работу за пять секунд. Пом­ни, что это по пять сер­веров вмес­то каж­дых двух!
  8. Фай­лы и про­чие бинар­ные дан­ные не сто­ит пихать стро­ками в XML/JSON/YAML, есть смысл зап­росить и передать их отдель­ным зап­росом. Самое бес­толко­вое занятие — запако­вывать боль­шой бинар­ный пакет в стро­ку, переко­дируя каж­дый байт, что­бы потом передать его сно­ва как бай­ты, но уже в тек­сто­вом про­токо­ле.
  9. Нет ничего зазор­ного в том, что­бы отка­зать­ся от чего‑то ненуж­ного или опци­ональ­ного. Нап­ример, совер­шенно необя­затель­но на каж­дый чих в сери­али­зации писать в лог или генери­ровать «Вой­ну и мир». Миними­зируй любые зат­раты на сери­али­зацию, не кор­ми Сери­али­затор сверх того, что он дол­жен получить как неиз­бежное зло.
  10. Нет никаких авто­рите­тов, за все­ми нуж­но про­верять, экспе­римен­тируй, замеряй, не верь никому — ни раз­работ­чикам биб­лиотек, ни авто­ру книг, ни авто­ру этой статьи, ни самому себе. Верь резуль­тату выпол­нения сво­его кода, верь толь­ко тем циф­рам, которые тебе нуж­но улуч­шить.

Не делай ничего лиш­него сверх того что ты дол­жен делать.

 

FIN

Итак, наш Сери­али­затор если не повер­жен, то уж точ­но оста­нет­ся недокор­млен­ным. Из неиз­бежно­го пожира­теля эффектив­ности при­ложе­ния он прев­ратил­ся в тво­его пос­лушно­го слу­гу. Мы прош­ли игру под наз­вани­ем Ака­демия C++ до кон­ца. Приш­ла пора тит­ров.

Мы вмес­те боролись с шаб­лонами и метап­рограм­мирова­нием, побеж­дали ста­тичес­кую типиза­цию и получа­ли динами­чес­кую типиза­цию в C++, мы опти­мизи­рова­ли про­цес­сы соз­дания новых объ­ектов, избавля­ясь от лиш­них обра­щений за памятью к куче, мы научи­лись работать с бай­тами и стро­ками как с раз­ными сущ­ностя­ми, а так­же поз­нали суть работы чисел с пла­вающей точ­кой. Сегод­няшний рас­сказ об эффектив­ной сери­али­зации завер­шает полуго­довой цикл лек­ций в нашей Ака­демии C++.

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

Не стес­няй­ся при­менять новые зна­ния! Ведь толь­ко путем проб и оши­бок ты получа­ешь бес­ценный и в чем‑то уни­каль­ный опыт. Ни одна кни­га и ни одна статья в жур­нале не заменит набитых тобой самим шишек. Дер­зай, про­буй! Веро­ятно, биб­лиоте­ки STL в свое вре­мя не было бы, если бы Алек­сандр Сте­панов не решил, что миру C++ не хва­тает биб­лиоте­ки с обоб­щенны­ми алго­рит­мами и удоб­ными кон­тей­нерами с общей логикой. Не думай, что опыт дает­ся с рож­дени­ем, он пря­мо про­пор­циона­лен прой­ден­ному тобой пути по дороге осво­ения новых воз­можнос­тей.

Глав­ное, что­бы то, что ты дела­ешь, то, что соз­даешь сам, тебе нра­вилось. Это зна­чит — ты на вер­ном пути. Так дер­жать!

Оставить мнение