对32位寄存器中的非零字符进行高效的UTF-8字符长度解码

时间:2016-12-21 14:00:33

标签: assembly utf-8 nasm x86-64 micro-optimization

我在eax中存储了一个UTF-8字符,稍后在处理中,我需要知道该字符组成了多少字节。

我已经缩小了这一点,最大限度地减少了班次和面具,并想知道我在某个地方错过了一些巧妙的伎俩?

选项1:暴力

    mov     r11, 4      ;   Maximum bytes
    bt      eax, 31     ;   Test 4th MSB
    jc      .exit 
    dec     r11         ;   Lets try 3
    bt      eax, 23     ;   Test 3rd MSB
    jc      .exit 
    dec     r11         ;   Lets try 2
    bt      eax, 15     ;   Test 2nd MSB
    jc      .exit 
    dec     r11         ;   It's straight up ascii (1 byte)
.exit:

注意:

  1. 我在eax注册表中的累积错误,正如大家所指出的那样。
  2. 玛格丽特和佩德7g都提供了解决方案,我学到的东西比预期的要多。

2 个答案:

答案 0 :(得分:3)

如果您可以假设correct encoding of the character,您只需检查第一个代码单元中最高零的位置(由于UTF-8的自动同步属性)。

罪魁祸首是,对于一个代码单元的代码点,最高零为第7位。对于 n 代码单元的代码点,最高位为7 - n (注意“不连续性”)。

假设第一个代码单元位于al

not al                 ;Trasform highest 0 in highest 1
bsr al, al             ;Find the index (from bit0) of the first 1 from the left
xor al, 7              ;Perform 7 - index
                       ;This gives 0 for single code unit code points
mov ah, 1
cmovz al, ah           ;Change back to 1

请注意,{0}没有为输入定义bsr,但只有无效的前导代码单元(值11111111b)才会出现{。}}。

您可以在jz <error handler>指令后使用bsr检测无效的0xff代码单元。

感谢@CodyGray指出原始版本的错误 感谢@PeterCorders指出要做7-AL的XOR技巧。

答案 1 :(得分:1)

如果你坚持颠倒的字节顺序(无论出于什么奇怪的原因),你仍然可以简单地扫描第一位设置为1,除以8和+1以获得字节数。

GetReversedShiftedUtf8BytesCount:
    ; eax = UTF8 code in reversed order, by from LSB
    ; 'É' (c3 89) => eax = 0x0000c389
    bsr ecx,eax
    cmovz ecx,eax   ; needed only for eax = 0
      ; ^ if eax is never 0 on input, this "cmovz" can be removed
    shr ecx,3
    inc ecx
    ret

当你将char的第一个字节放入MSB时,它将为多字节字符产生15,23或31位数,对于7b ASCII,bsr将产生0到6之间的任何字符。 “div 8”将直接修复它们,无论哪种方式,它都不关心。

此例程实际上也适用于有效普通UTF8代码。

对于以<0>字节结尾的无效 UTF8代码,它将返回错误的字节数(没有零字节)。

当然总是也可以使用LUT解决方案:

    movzx  ecx,al
    shr    ecx,3
    movzx  ecx,byte [utf8lengthLUT + ecx]  ; +rcx for 64b
    ; ecx = number of bytes or 0 for invalid leading byte value
    ...

utf8lengthLUT:                     ; 32B look-up table for upper 5b of 1st byte
    db     1, 1, 1, 1, 1, 1, 1, 1  ; 00000 - 00111 ; single byte
    db     1, 1, 1, 1, 1, 1, 1, 1  ; 01000 - 01111 ; single byte
    db     0, 0, 0, 0, 0, 0, 0, 0  ; 10000 - 10111 ; not valid leading byte
    db     2, 2, 2, 2              ; 11000 - 11011 ; two bytes code point
    db     3, 3                    ; 11100 - 11101 ; three bytes code point
    db     4                       ; 11110         ; four bytes code point
    db     0                       ; 11111         ; not valid leading byte

我没有调试它,只是尝试使用nasm进行语法检查。我当然也没有描述它。 :)看看那个bsr变种的短缺,我怀疑即使在bsr伤害的CPU上,这也会非常快。

但是这个以不同的方式处理无效的UTF8操作码,而不是检测非零MSB并返回它的数量+ 1(对前导字节内容不敏感),它将正确解码前导字节信息并在前导位为0时返回0错误。但正确的前导位(错误的第二个+字节(如c3 00))仍将返回2,而第一个变量在这种情况下返回1

(如果您不关心无效11111前导字节信息,则可以只使用16B LUT表,并将其视为4字节代码点)

顺便说一句,有一些i18n库(开源),做所有这些事情,如验证utf8输入,修复无效的,计算字符等等......其中一些已经存在了十多年......然而仍然得到错误报告和修复。这是一种微妙的暗示,正确编写这些东西是多么困难(没有将应用程序暴露给某些输入数据漏洞)。 :)

(加上考虑了多少(修复)编辑收到这两个答案...... :))

还有一个offtopic建议:如果你试图用PHP写一些东西,应该处理UTF8输入数据(不是来自可靠来源,甚至来自可靠来源),特别是如果那些输入数据来自GET / POST响应......不要靠自己。决不。获得一个框架。 :)