Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз

Егер сіз әзірлеуші ​​болсаңыз және сіз кодтауды таңдау міндетіне тап болсаңыз, Юникод әрқашан дерлік дұрыс шешім болады. Нақты ұсыну әдісі контекстке байланысты, бірақ көбінесе мұнда әмбебап жауап бар - UTF-8. Оның жақсы жағы - бұл барлық Юникод таңбаларын шығынсыз пайдалануға мүмкіндік береді тым көп көп жағдайда көп байт. Рас, латын әліпбиін ғана қолданатын тілдер үшін «тым көп емес» әр таңбаға екі байт. Бізді бар болғаны 256 қол жетімді таңбамен шектейтін тарихқа дейінгі кодтауларға оралмай, жақсырақ істей аламыз ба?

Төменде мен осы сұраққа жауап беру әрекетіммен танысуды және UTF-8-дегі артықшылықты қоспай, әлемнің көптеген тілдерінде жолдарды сақтауға мүмкіндік беретін салыстырмалы түрде қарапайым алгоритмді енгізуді ұсынамын.

Жауапкершіліктен бас тарту. Мен бірден бірнеше маңызды ескертпелер жасаймын: сипатталған шешім UTF-8 үшін әмбебап ауыстыру ретінде ұсынылмайды, ол істердің тар тізімінде ғана жарамды (олар туралы толығырақ төменде) және оны ешбір жағдайда үшінші тарап API интерфейстерімен (бұл туралы тіпті білмейтін) өзара әрекеттесу үшін қолдануға болмайды. Көбінесе жалпы мақсаттағы қысу алгоритмдері (мысалы, дефлат) мәтіндік деректердің үлкен көлемін ықшам сақтау үшін қолайлы. Сонымен қатар, өз шешімімді жасау барысында мен Юникодтың өзінде бар стандартты таптым, ол дәл сол мәселені шешеді - бұл біршама күрделірек (және көбінесе одан да нашар), бірақ бәрібір бұл қабылданған стандарт және жай ғана қойылған емес. тізеде бірге. Ол туралы да айтамын.

Юникод және UTF-8 туралы

Бастау үшін бұл не туралы бірнеше сөз Юникод и UTF-8.

Өздеріңіз білетіндей, бұрын 8-биттік кодтау танымал болды. Олардың көмегімен бәрі қарапайым болды: 256 таңбаны 0-ден 255-ке дейінгі сандармен нөмірлеуге болады, ал 0-ден 255-ке дейінгі сандарды бір байтпен көрсетуге болады. Егер біз ең басына оралсақ, ASCII кодтауы толығымен 7 битпен шектелген, сондықтан оның байтты көрсетуіндегі ең маңызды бит нөлге тең, ал 8 биттік кодтаулардың көпшілігі онымен үйлесімді (олар тек «жоғарғы» разрядта ғана ерекшеленеді) бөлігі, мұнда ең маңызды бит бір ).

Юникодтың бұл кодтаулардан айырмашылығы неде және онымен байланыстырылған нақты көрсетілімдер неліктен - UTF-8, UTF-16 (BE және LE), UTF-32? Оны ретімен реттеп көрейік.

Негізгі Юникод стандарты тек таңбалар арасындағы сәйкестікті (кейбір жағдайларда таңбалардың жеке құрамдас бөліктері) және олардың сандарын сипаттайды. Және бұл стандартта көптеген мүмкін сандар бар - бастап 0x00 қарай 0x10FFFF (1 114 112 дана). Егер біз осындай диапазондағы санды айнымалыға қойғымыз келсе, бізге 1 немесе 2 байт жеткіліксіз болар еді. Біздің процессорларымыз үш байттық сандармен жұмыс істеуге арналмағандықтан, біз әр таңбаға 4 байтты қолдануға мәжбүр боламыз! Бұл UTF-32, бірақ дәл осы «ысырапшылдыққа» байланысты бұл формат танымал емес.

Бақытымызға орай, Юникодтағы таңбалардың реті кездейсоқ емес. Олардың бүкіл жиынтығы 17-ге бөлінгенұшақтар", олардың әрқайсысында 65536 (0x10000) «код нүктелері" Мұнда «код нүктесі» түсінігі қарапайым таңба саны, оған Юникод арқылы тағайындалған. Бірақ, жоғарыда айтылғандай, Юникодта тек жеке таңбалар ғана емес, сонымен қатар олардың құрамдас бөліктері мен қызмет көрсету белгілері де нөмірленеді (және кейде санға ештеңе сәйкес келмейді - мүмкін әзірге, бірақ біз үшін бұл соншалықты маңызды емес), сондықтан әрқашан таңбалар емес, сандардың өздері туралы нақты айту дұрысырақ. Дегенмен, төменде қысқалық үшін мен «код нүктесі» терминін білдіретін «символ» сөзін жиі қолданатын боламын.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Юникодты ұшақтар. Көріп отырғаныңыздай, оның көп бөлігі (4-тен 13-ке дейінгі ұшақтар) әлі де пайдаланылмаған.

Ең қызығы, барлық негізгі «целлюлоза» нөлдік жазықтықта жатыр, ол «деп аталады.Негізгі көптілді ұшақ". Егер жолда қазіргі тілдердің бірінде (соның ішінде қытай тілінде) мәтін болса, сіз бұл жазықтықтан шықпайсыз. Бірақ Юникодтың қалған бөлігін де кесіп тастай алмайсыз - мысалы, эмодзилер негізінен тілдің соңында орналасқан. келесі ұшақ»,Қосымша көптілді ұшақ«(ол созылады 0x10000 қарай 0x1FFFF). Сонымен, UTF-16 мұны жасайды: барлық таңбалар ішіне кіреді Негізгі көптілді ұшақ, сәйкес екі байттық нөмірмен "сол күйінде" кодталған. Дегенмен, осы диапазондағы кейбір сандар нақты таңбаларды көрсетпейді, бірақ осы байт жұбынан кейін басқасын қарастыру керек екенін көрсетеді - осы төрт байттың мәндерін біріктіру арқылы біз жабатын санды аламыз. бүкіл жарамды Юникод ауқымы. Бұл идея «суррогат жұптар» деп аталады - сіз олар туралы естіген боларсыз.

Сондықтан UTF-16 бір «код нүктесіне» екі немесе (өте сирек жағдайларда) төрт байт қажет. Бұл төрт байтты үнемі пайдаланғаннан жақсырақ, бірақ латын тілі (және басқа ASCII таңбалары) осылайша кодталғанда жарты кеңістікті нөлге жұмсайды. UTF-8 мұны түзетуге арналған: онда ASCII бұрынғыдай тек бір байтты алады; кодтары 0x80 қарай 0x7FF - екі байт; бастап 0x800 қарай 0xFFFF - үш, және бастап 0x10000 қарай 0x10FFFF - төрт. Бір жағынан, латын әліпбиі жақсы болды: ASCII-мен үйлесімділік қайтарылды және тарату 1-ден 4 байтқа дейін біркелкі «таралды». Бірақ латын тілінен басқа әліпбилер, өкінішке орай, UTF-16-мен салыстырғанда ешқандай пайда әкелмейді және қазір көбіне екі байттың орнына үш байт қажет - екі байттық жазбамен қамтылған диапазон 32 есе қысқарды. 0xFFFF қарай 0x7FF, және оған қытай тілі де, мысалы, грузин тілі де кірмейді. Кириллица және басқа бес алфавит - hurray - сәттілік, әр таңбаға 2 байт.

Неліктен бұл орын алады? UTF-8 таңба кодтарын қалай көрсететінін көрейік:
Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Тікелей сандарды көрсету үшін мұнда таңбамен белгіленген биттер қолданылады x. Екі байттық жазбада мұндай 11 бит (16-дан) ғана болатынын көруге болады. Мұндағы жетекші биттердің тек көмекші функциясы бар. Төрт байт жазба жағдайында код нүктесінің нөмірі үшін 21 биттің 32-і бөлінген - үш байт (барлығы 24 бит беретін) жеткілікті сияқты көрінеді, бірақ қызмет көрсету маркерлері тым көп жейді.

Бұл жаман ба? Онша емес. Бір жағынан, егер біз кеңістікке көп көңіл бөлетін болсақ, бізде барлық қосымша энтропия мен артықшылықты оңай жоя алатын қысу алгоритмдері бар. Екінші жағынан, Юникодтың мақсаты мүмкін болатын әмбебап кодтауды қамтамасыз ету болды. Мысалы, біз UTF-8-де кодталған жолды бұрын тек ASCII-мен жұмыс істеген кодқа сеніп тапсыра аламыз және ол ASCII диапазонында жоқ таңбаны көреді деп қорықпаймыз (айткенде, UTF-8-де барлығында). нөлдік биттен басталатын байт - бұл дәл ASCII). Ал егер біз кенеттен үлкен жолдан кішкене құйрықты басынан бастап декодтаусыз кесіп алғымыз келсе (немесе зақымдалған бөлімнен кейін ақпараттың бір бөлігін қалпына келтірсек), таңба басталатын ығысуды табу оңай (жеткілікті) бит префиксі бар байтты өткізіп жіберу 10).

Неліктен жаңа нәрсе ойлап табады?

Сонымен қатар, кейде deflate сияқты қысу алгоритмдері нашар қолданылатын жағдайлар болады, бірақ сіз жолдардың жинақы сақталуына қол жеткізгіңіз келеді. Жеке өзім бұл мәселеге құрылыс туралы ойлағанда тап болдым қысылған префикс ағашы еркін тілдердегі сөздерді қамтитын үлкен сөздік үшін. Бір жағынан, әрбір сөз өте қысқа, сондықтан оны қысу тиімсіз болады. Екінші жағынан, мен қарастырған ағашты іске асыру сақталған жолдың әрбір байты бөлек ағаш шыңын жасайтын етіп жасалған, сондықтан олардың санын азайту өте пайдалы болды. Менің кітапханамда Az.js (Сол сияқты пиморфия 2, ол негізделген) ұқсас мәселені қарапайым шешуге болады - жолдар ішіне оралған DAWG-сөздік, сол жерде сақталған жақсы ескі CP1251. Бірақ, түсінуге оңай, бұл шектеулі алфавит үшін ғана жақсы жұмыс істейді - мұндай сөздікке қытай тіліндегі жолды қосу мүмкін емес.

Мен осындай деректер құрылымында UTF-8 пайдалану кезінде туындайтын тағы бір жағымсыз нюансты бөлек атап өткім келеді. Жоғарыдағы суретте таңба екі байт ретінде жазылғанда оның нөміріне қатысты биттер қатарда келмейтіні, бірақ жұп битпен бөлінгені көрсетілген. 10 ортасында: 110xxxxx 10xxxxxx. Осыған байланысты таңба кодында екінші байттың төменгі 6 биті толып кеткенде (яғни, ауысу орын алады. 1011111110000000), содан кейін бірінші байт өзгереді. «р» әрпі байтпен белгіленеді екен 0xD0 0xBF, ал келесі «r» қазірдің өзінде 0xD1 0x80. Префикс ағашында бұл негізгі түйіннің екіге бөлінуіне әкеледі - біреуі префикс үшін 0xD0, және басқасы үшін 0xD1 (бірақ бүкіл кирилл әліпбиі тек екінші байт арқылы кодталады).

Мен не алдым

Осы мәселемен бетпе-бет келіп, мен биттермен ойын ойнауға жаттығып, сонымен бірге жалпы Юникод құрылымымен жақсырақ танысуды шештім. Нәтиже UTF-C кодтау пішімі болды («C» үшін тығыз), ол бір код нүктесіне 3 байттан аспайды және өте жиі тек жұмсауға мүмкіндік береді бүкіл кодталған жол үшін қосымша бір байт. Бұл көптеген ASCII емес алфавиттерде мұндай кодтау болатынына әкеледі UTF-30-ге қарағанда 60-8% ықшам.

Мен формада кодтау және декодтау алгоритмдерін жүзеге асыру мысалдарын ұсындым JavaScript және Go кітапханалары, сіз оларды кодыңызда еркін пайдалана аласыз. Бірақ мен әлі де бұл формат белгілі бір мағынада «велосипед» болып қала беретінін атап өтемін және оны пайдалануды ұсынбаймын. не үшін керек екенін түсінбей. Бұл әлі де «UTF-8-ді жақсартудан» гөрі эксперимент. Соған қарамастан, ондағы код ұқыпты, қысқаша жазылған, көптеген пікірлер мен сынақ қамтуы бар.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Тест нәтижелері және UTF-8 салыстыру

Мен де жасадым демо беті, мұнда алгоритмнің өнімділігін бағалауға болады, содан кейін мен оның принциптері мен даму процесі туралы көбірек айтып беремін.

Артық биттерді жою

Мен UTF-8-ді негізге алдым, әрине. Онда өзгертуге болатын бірінші және ең айқын нәрсе - әрбір байттағы қызмет биттерінің санын азайту. Мысалы, UTF-8 ішіндегі бірінші байт әрқашан екеуінен басталады 0, немесе бірге 11 - префикс 10 Тек келесі байттарда ғана бар. Префиксті ауыстырайық 11 туралы 1, ал келесі байттар үшін біз префикстерді толығымен алып тастаймыз. Не болады?

0xxxxxxx — 1 байт
10xxxxxx xxxxxxxx - 2 байт
110xxxxx xxxxxxxx xxxxxxxx - 3 байт

Күте тұрыңыз, төрт байттық жазба қайда? Бірақ бұл енді қажет емес - үш байтпен жазғанда, бізде қазір 21 бит қол жетімді және бұл барлық сандар үшін жеткілікті 0x10FFFF.

Біз мұнда нені құрбан еттік? Ең бастысы - буфердегі ерікті орыннан таңба шекараларын анықтау. Біз ерікті байтты көрсетіп, одан келесі таңбаның басын таба алмаймыз. Бұл біздің форматымыздың шектеуі, бірақ іс жүзінде бұл сирек қажет. Біз әдетте буферді басынан бастап өткізе аламыз (әсіресе қысқа жолдарға қатысты).

Тілдерді 2 байтпен қамту жағдайы да жақсарды: енді екі байт форматы 14 бит ауқымын береді және бұл кодтар 0x3FFF. Қытайлықтардың жолы жоқ (олардың кейіпкерлері негізінен мыналардан тұрады 0x4E00 қарай 0x9FFF), бірақ грузиндер және басқа да көптеген халықтар көбірек көңілді - олардың тілдері де әр таңбаға 2 байтқа сәйкес келеді.

Кодер күйін енгізіңіз

Енді сызықтардың қасиеттері туралы ойланайық. Сөздікте бір алфавиттің таңбаларымен жазылған сөздер жиі кездеседі және бұл көптеген басқа мәтіндерге де қатысты. Бұл әліпбиді бір рет көрсетіп, оның ішіндегі әріптің нөмірін ғана көрсетсеңіз жақсы болар еді. Юникод кестесіндегі таңбалардың орналасуы бізге көмектесетінін көрейік.

Жоғарыда айтылғандай, Юникод бөлінеді ұшақ Әрқайсысы 65536 код. Бірақ бұл өте пайдалы бөлім емес (жоғарыда айтылғандай, біз көбінесе нөлдік жазықтықта боламыз). Ең қызықтысы - бөлу блоктар. Бұл диапазондар енді бекітілген ұзындыққа ие емес және мағыналырақ – әдетте, әрқайсысы бір алфавиттегі таңбаларды біріктіреді.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Бенгал алфавитінің таңбалары бар блок. Өкінішке орай, тарихи себептерге байланысты бұл өте тығыз емес қаптаманың мысалы - 96 таңба 128 блок-код нүктесінде ретсіз шашыраңқы.

Блоктардың басталуы және олардың өлшемдері әрқашан 16-ға еселік болып табылады - бұл жай ғана ыңғайлы болу үшін жасалады. Сонымен қатар, көптеген блоктар 128 немесе тіпті 256-ға еселік мәндермен басталады және аяқталады - мысалы, негізгі кириллица алфавитінен 256 байт алады. 0x0400 қарай 0x04FF. Бұл өте ыңғайлы: егер префиксті бір рет сақтасақ 0x04, онда кез келген кириллица таңбасын бір байтқа жазуға болады. Рас, осылайша біз ASCII-ге (және жалпы кез келген басқа таңбаларға) оралу мүмкіндігін жоғалтамыз. Сондықтан біз мұны істейміз:

  1. Екі байт 10yyyyyy yxxxxxxx таңбаны санмен белгілеп қана қоймайды yyyyyy yxxxxxxx, бірақ сонымен бірге өзгереді ағымдағы алфавит туралы yyyyyy y0000000 (яғни біз ең аз маңыздыларынан басқа барлық биттерді есте сақтаймыз 7 бит);
  2. Бір байт 0xxxxxxx бұл қазіргі әліпбидің сипаты. Оны тек 1-қадамда есте қалған офсетке қосу керек. Біз әліпбиді өзгертпегенімізбен, офсет нөлге тең, сондықтан ASCII үйлесімділігін сақтадық.

Сол сияқты 3 байтты қажет ететін кодтар үшін:

  1. Үш байт 110yyyyy yxxxxxxx xxxxxxxx саны бар таңбаны көрсетіңіз yyyyyy yxxxxxxx xxxxxxxx, өзгерту ағымдағы алфавит туралы yyyyyy y0000000 00000000 (кішілерден басқаның бәрі есіне түсті 15 бит) және біз қазір кіретін құсбелгіні қойыңыз ұзақ режимі (алфавитті екі байтқа ауыстырған кезде біз бұл жалаушаны қалпына келтіреміз);
  2. Екі байт 0xxxxxxx xxxxxxxx ұзақ режимде бұл ағымдағы алфавиттің сипаты. Сол сияқты, біз оны 1-қадамдағы ығысумен қосамыз. Жалғыз айырмашылығы, қазір біз екі байтты оқимыз (өйткені біз бұл режимге ауыстық).

Жақсы естіледі: енді бірдей 7-биттік Юникод ауқымындағы таңбаларды кодтау керек болғанымен, біз басында 1 қосымша байт және әр таңбаға барлығы бір байт жұмсаймыз.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Бұрынғы нұсқалардың бірінен жұмыс істеу. Ол UTF-8-ді жиі жеңеді, бірақ әлі де жақсартуға мүмкіндік бар.

Сорақысы несі? Біріншіден, бізде шарт бар, атап айтқанда ағымдағы алфавиттің ығысуы және құсбелгі қойыңыз ұзақ режим. Бұл бізді одан әрі шектейді: енді бірдей таңбаларды әртүрлі контексттерде әртүрлі кодтауға болады. Ішкі жолдарды іздеу, мысалы, байттарды салыстыру арқылы емес, осыны ескере отырып жасалуы керек. Екіншіден, біз әліпбиді ауыстыра салысымен, ASCII таңбаларының кодталуы нашарлады (және бұл тек латын әліпбиі емес, сонымен қатар бос орындарды қоса алғанда, негізгі тыныс белгілері) - олар әліпбиді қайтадан 0-ге өзгертуді талап етеді, яғни, қайтадан қосымша байт (содан кейін негізгі ойымызға оралу үшін тағы бір).

Бір әліпби жақсы, екеуі жақсы

Жоғарыда сипатталған үшеуіне тағы біреуін қысып, бит префикстерімізді сәл өзгертуге тырысайық:

0xxxxxxx — қалыпты режимде 1 байт, ұзақ режимде 2 байт
11xxxxxx — 1 байт
100xxxxx xxxxxxxx - 2 байт
101xxxxx xxxxxxxx xxxxxxxx - 3 байт

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз

Енді екі байттық жазбада бір аз қолжетімді бит бар - код дейін көрсетеді 0x1FFF, жоқ 0x3FFF. Дегенмен, ол қос байт UTF-8 кодтарына қарағанда айтарлықтай үлкен, ең көп таралған тілдер әлі де сәйкес келеді, ең елеулі жоғалту төмендеді хирагана и катакана, жапондар қайғылы.

Бұл жаңа код дегеніміз не? 11xxxxxx? Бұл 64 таңбадан тұратын шағын «сақта», ол біздің негізгі әліпбиімізді толықтырады, сондықтан мен оны көмекші деп атадым (көмекші) алфавит. Ағымдағы әліпбиді ауыстырған кезде ескі әліпбидің бір бөлігі көмекші болады. Мысалы, біз ASCII-ден кириллицаға ауыстырдық - қазір қоймада 64 таңба бар Латын әліпбиі, сандар, бос орын және үтір (ASCII емес мәтіндердегі ең жиі кірістірулер). ASCII-ге қайта ауысыңыз - және кириллицаның негізгі бөлігі көмекші әліпбиге айналады.

Екі алфавитке қол жеткізудің арқасында біз алфавиттерді ауыстыру үшін минималды шығындармен көптеген мәтіндерді өңдей аламыз (тыныс белгілері көбінесе ASCII-ге оралуға әкеледі, бірақ содан кейін біз қосымша әліпбиден ASCII емес көптеген таңбаларды аламыз. қайта ауысу).

Бонус: ішкі алфавиттің префиксі 11xxxxxx және оның бастапқы ығысуын таңдау 0xC0, біз CP1252-мен ішінара үйлесімділікке ие боламыз. Басқаша айтқанда, CP1252-де кодталған көптеген (бірақ барлығы емес) Батыс Еуропалық мәтіндер UTF-C тілінде бірдей көрінеді.

Алайда, бұл жерде қиындық туындайды: негізгі әліпбиден көмекші әліпбиді қалай алуға болады? Сіз сол офсетті қалдыра аласыз, бірақ, өкінішке орай, мұнда Юникод құрылымы бізге қарсы ойнап жатыр. Көбінесе алфавиттің негізгі бөлігі блоктың басында болмайды (мысалы, ресейлік «А» астанасында код бар. 0x0410, дегенмен кириллица блогы басталады 0x0400). Осылайша, алғашқы 64 таңбаны қоймаға енгізгеннен кейін біз әліпбидің құйрық бөлігіне кіру мүмкіндігін жоғалтуымыз мүмкін.

Бұл мәселені шешу үшін мен әртүрлі тілдерге сәйкес келетін кейбір блоктарды қолмен аралап шықтым және олар үшін негізгі ішіндегі көмекші алфавиттің ығысуын көрсеттім. Ерекшелік ретінде латын әліпбиі, әдетте, base64 сияқты қайта реттелген.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз

Соңғы жанасулар

Ақырында, бір нәрсені қай жерде жақсартуға болатынын ойланайық.

формат екенін ескеріңіз 101xxxxx xxxxxxxx xxxxxxxx дейінгі сандарды кодтауға мүмкіндік береді 0x1FFFFF, және Юникод ертерек аяқталады, сағат 0x10FFFF. Басқаша айтқанда, соңғы код нүктесі ретінде көрсетіледі 10110000 11111111 11111111. Сондықтан, егер бірінші байт формада болса деп айта аламыз 1011xxxx (Қайда xxxx 0-ден үлкен), онда бұл басқа нәрсені білдіреді. Мысалы, бір байтта кодтау үшін үнемі қол жетімді тағы 15 таңбаны қосуға болады, бірақ мен мұны басқаша жасауды шештім.

Енді үш байтты қажет ететін Юникод блоктарын қарастырайық. Негізінде, жоғарыда айтылғандай, бұл қытай таңбалары - бірақ олармен ештеңе істеу қиын, олардың 21 мыңы бар. Бірақ хирагана мен катакана да сонда ұшты - енді олардың саны онша көп емес, екі жүзден аз. Жапондықтарды еске түсіргендіктен, эмодзилер де бар (шын мәнінде олар Юникодта көптеген жерлерде шашыраңқы, бірақ негізгі блоктар ауқымда. 0x1F300 - 0x1FBFF). Егер сіз қазір бірден бірнеше код нүктелерінен жиналған эмодзилер бар екенін ойласаңыз (мысалы, эмодзи ‍‍‍)Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз 7 кодтан тұрады!), Содан кейін әрқайсысына үш байт жұмсау ұят болады (бір белгіше үшін 7×3 = 21 байт, қорқынышты арман).

Сондықтан эмодзилерге, хираганаға және катаканаға сәйкес бірнеше таңдалған диапазондарды таңдаймыз, оларды бір үздіксіз тізімге қайта нөмірлейміз және оларды үш емес, екі байт ретінде кодтаймыз:

1011xxxx xxxxxxxx

Керемет: жоғарыда аталған эмодзиТағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз, 7 кодтық нүктеден тұратын, UTF-8-де 25 байт алады және біз оны орналастырамыз 14 (әр код нүктесі үшін дәл екі байт). Айтпақшы, Хабр оны қорытудан бас тартты (ескіде де, жаңа редакторда да), сондықтан мен оны суретпен енгізуге тура келді.

Тағы бір мәселені шешуге тырысайық. Біздің есімізде, негізгі әліпби - бұл жоғары 6 бит, біз оны есте сақтаймыз және әрбір келесі декодталған символдың кодына жабыстырамыз. Блоктағы қытай таңбалары жағдайында 0x4E00 - 0x9FFF, бұл 0 немесе 1 бит. Бұл өте ыңғайлы емес: біз осы екі мән арасында әліпбиді үнемі ауыстырып отыруымыз керек (яғни, үш байт жұмсау). Есіңізде болсын, ұзын режимде кодтың өзінен біз қысқа режимді пайдаланып кодтайтын таңбалар санын шегеруге болады (жоғарыда сипатталған барлық амалдардан кейін бұл 10240) - содан кейін иероглифтер ауқымы келесіге ауысады. 0x2600 - 0x77FF, және бұл жағдайда, осы бүкіл ауқымда ең маңызды 6 бит (21-ден) 0-ге тең болады. Осылайша, иероглифтер тізбегі әр иероглифке екі байт (мұндай үлкен диапазон үшін оңтайлы) пайдаланады. алфавиттік ауысуларды тудырады.

Балама шешімдер: SCSU, BOCU-1

Юникод сарапшылары мақаланың тақырыбын оқып шыққаннан кейін, Юникод стандарттарының арасында тікелей бар екенін еске салады. Юникод үшін стандартты қысу схемасы (SCSU), ол мақалада сипатталғанға өте ұқсас кодтау әдісін сипаттайды.

Шынымды айтсам, оның бар екенін мен өз шешімімді жазуға терең бойлаған соң ғана білдім. Егер мен бұл туралы басынан білсем, мен өз көзқарасымды ойлап табудың орнына іске асыруды жазуға тырысар едім.

Бір қызығы, SCSU мен өзім ойлап тапқан идеяларға өте ұқсас идеяларды қолданады («алфавит» ұғымының орнына олар «терезені» пайдаланады және мендегінен де көп олар бар). Сонымен қатар, бұл форматтың кемшіліктері де бар: ол кодтауға қарағанда қысу алгоритмдеріне біршама жақынырақ. Атап айтқанда, стандарт көптеген ұсыну әдістерін береді, бірақ оңтайлысын қалай таңдау керектігін айтпайды - бұл үшін кодтаушы эвристиканың қандай да бір түрін қолдануы керек. Осылайша, жақсы қаптаманы шығаратын SCSU кодтары менің алгоритміме қарағанда күрделірек және ауыр болады.

Салыстыру үшін мен SCSU-ның салыстырмалы түрде қарапайым іске асырылуын JavaScript-ке ауыстырдым - код көлемі бойынша ол менің UTF-C-мен салыстыруға болатын болды, бірақ кейбір жағдайларда нәтиже ондаған пайызға нашар болды (кейде ол одан асып кетуі мүмкін, бірақ көп емес). Мысалы, иврит және грек тілдеріндегі мәтіндер UTF-C арқылы кодталған SCSU-дан 60% жақсы (мүмкін олардың ықшам алфавиттеріне байланысты).

Сонымен қатар, мен SCSU-дан басқа Юникодты жинақы түрде көрсетудің тағы бір жолы бар екенін қосамын - BOCU-1, бірақ ол MIME үйлесімділігіне бағытталған (бұл маған қажет емес) және кодтауға сәл басқаша көзқарасты қолданады. Мен оның тиімділігін бағалаған жоқпын, бірақ менің ойымша, ол SCSU-дан жоғары болуы екіталай.

Мүмкін жақсартулар

Мен ұсынған алгоритм дизайн бойынша әмбебап емес (бұл менің мақсаттарым Юникод консорциумының мақсаттарынан ең көп алшақтататын жері болса керек). Оның ең алдымен бір тапсырма (көп тілді сөздікті префикс ағашында сақтау) үшін әзірленгенін жоғарыда айттым және оның кейбір мүмкіндіктері басқа тапсырмаларға сәйкес келмеуі мүмкін. Бірақ бұл стандарт емес екендігі плюс болуы мүмкін - оны қажеттіліктеріңізге сай оңай өзгертуге болады.

Мысалы, айқын жолмен сіз күйдің болуынан құтыла аласыз, азаматтығы жоқ кодтауды жасай аласыз - айнымалы мәндерді жаңартпаңыз. offs, auxOffs и is21Bit кодтауышта және дешифраторда. Бұл жағдайда бір алфавиттің таңбалар тізбегін тиімді жинақтау мүмкін болмайды, бірақ контекстке қарамастан бірдей таңба әрқашан бірдей байттармен кодталатынына кепілдік болады.

Бұған қоса, сіз әдепкі күйді өзгерту арқылы кодтауышты белгілі бір тілге бейімдей аласыз - мысалы, орыс мәтіндеріне назар аудару, кодтауыш пен декодерді басында орнату offs = 0x0400 и auxOffs = 0. Бұл, әсіресе, азаматтығы жоқ режим жағдайында мағынасы бар. Жалпы алғанда, бұл ескі сегіз разрядты кодтауды қолдануға ұқсас болады, бірақ қажет болған жағдайда барлық Юникодтан таңбаларды енгізу мүмкіндігін жоймай.

Жоғарыда айтылған тағы бір кемшілік - UTF-C тілінде кодталған үлкен мәтінде ерікті байтқа жақын таңбалар шекарасын табудың жылдам жолы жоқ. Егер сіз кодталған буферден соңғы, айталық, 100 байтты кесіп тастасаңыз, сіз ештеңе істей алмайтын қоқыс алу қаупіне ұшырайсыз. Кодтау көп гигабайттық журналдарды сақтауға арналмаған, бірақ жалпы оны түзетуге болады. Байт 0xBF ешқашан бірінші байт ретінде көрінбеуі керек (бірақ екінші немесе үшінші болуы мүмкін). Сондықтан кодтау кезінде ретті енгізуге болады 0xBF 0xBF 0xBF әрбір, айталық, 10 КБ - содан кейін, шекараны табу қажет болса, ұқсас маркер табылғанша таңдалған бөлікті сканерлеу жеткілікті болады. Соңғысының артынан 0xBF кейіпкердің бастауы болатынына кепілдік беріледі. (Декодтау кезінде бұл үш байт тізбегі, әрине, елемеу керек.)

қорытындылай келе

Осы уақытқа дейін оқыған болсаңыз, құттықтаймыз! Сіз мен сияқты Юникод құрылымы туралы жаңа нәрсе білдіңіз (немесе жадыңызды жаңарттыңыз) деп үміттенемін.

Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз
Демо беті. Иврит тілінің мысалы UTF-8 және SCSU екеуінен де артықшылықтарды көрсетеді.

Жоғарыда сипатталған зерттеулер стандарттарға қол сұғушылық ретінде қарастырылмауы керек. Дегенмен, мен өз жұмысымның нәтижесіне жалпы көңілім толады, сондықтан мен оларға ризамын үлесі: мысалы, кішірейтілген JS кітапханасының салмағы небәрі 1710 байт (және, әрине, тәуелділіктері жоқ). Жоғарыда айтқанымдай, оның жұмысын мына жерден табуға болады демо беті (сонымен қатар оны UTF-8 және SCSU-мен салыстыруға болатын мәтіндер жинағы бар).

Соңында, мен UTF-C қолданылатын жағдайларға тағы да назар аударамын бұл қажеті жоқ:

  • Жолдарыңыз жеткілікті ұзын болса (100-200 таңбадан). Бұл жағдайда deflate сияқты қысу алгоритмдерін пайдалану туралы ойлану керек.
  • Қажет болса ASCII мөлдірлігі, яғни кодталған реттіліктерде бастапқы жолда жоқ ASCII кодтары болмауы сіз үшін маңызды. Егер үшінші тарап API интерфейстерімен әрекеттесу кезінде (мысалы, дерекқормен жұмыс істеу) кодтау нәтижесін жолдар ретінде емес, байттардың дерексіз жиыны ретінде берсеңіз, мұның қажеттілігін болдырмауға болады. Әйтпесе, сіз күтпеген осалдықтарды алу қаупі бар.
  • Таңбалар шекараларын ерікті ығысу кезінде жылдам таба алғыңыз келсе (мысалы, жолдың бөлігі зақымдалған кезде). Мұны жасауға болады, бірақ жолды басынан сканерлеу арқылы (немесе алдыңғы бөлімде сипатталған өзгертуді қолдану).
  • Жолдардың мазмұнымен операцияларды жылдам орындау қажет болса (сұрыптау, олардан ішкі жолдарды іздеу, біріктіру). Бұл алдымен жолдарды декодтауды талап етеді, сондықтан UTF-C бұл жағдайларда UTF-8-ге қарағанда баяу болады (бірақ қысу алгоритмдерінен жылдамырақ). Бір жол әрқашан бірдей кодталғандықтан, декодтауды дәл салыстыру қажет емес және оны байт-байт негізінде жасауға болады.

жаңарту: пайдаланушы Тиомич төмендегі түсініктемелерде UTF-C қолдану шегін көрсететін графикті орналастырды. Ол оралған жол қысқа болғанша UTF-C жалпы мақсаттағы қысу алгоритміне (LZW нұсқасы) қарағанда тиімдірек екенін көрсетеді. ~140 таңба (бірақ, салыстыру бір мәтін бойынша жүргізілгенін ескертемін; басқа тілдер үшін нәтиже әртүрлі болуы мүмкін).
Тағы бір велосипед: біз Юникод жолдарын UTF-30-ге қарағанда 60-8% ықшам сақтаймыз

Ақпарат көзі: www.habr.com

пікір қалдыру