【问题标题】:Efficient UTF-8 character-length decoding for a non-zero character in a 32 bit register对 32 位寄存器中的非零字符进行高效的 UTF-8 字符长度解码
【发布时间】:2016-12-21 14:00:32
【问题描述】:

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

我已经缩小了范围,以最大限度地减少班次和蒙版,并想知道我是否在某处遗漏了一些巧妙的技巧?

选项 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. Margaret 和 Ped7g 都提供了解决方案,我学到的东西比预期的还要多。

【问题讨论】:

  • AFAIK 你应该从另一个方向检查,但不清楚你的角色是如何开始的。
  • @Jester 在读取 UTF-8(或 ascii)时,我会按字符顺序移动,因此如果是 UTF-8,则控制字节(例如 0x11 ......)位于较高的位置位置后跟延续(例如 0x10 ......)。如果我明白你的意思,那么首先检查 ascii 或成本最低的 UTF-8 的低阶会更有效?
  • 等等,那么eax 中的内容是什么,你能举一些例子吗?我确实期望ÉÉ 出现类似0x89c389c3 的内容(一个字符É 以下一个字符开头,恰好也是É 并适合eax 的剩余两个字节)。在这种情况下,您的代码会读取什么? (文本有字节c3 89 c3 89)如果您要为每个长度提供示例,最好验证有关这些的任何建议。 (加上你在备用寄存器上有多紧)
  • @Ped7g - 我在角色级别,你有两个(例如É,然后是第二个É)。所以对于一 (1) 个字符:rax = 0x000000000000c3a1
  • 是您的c3a1 (á) 错字,还是我仍然想念您的价值转换是如何工作的?如果您要按字符进行,则已经解析了该字符。顺便说一句,为什么有些东西没有提供长度?然后你“解析”它两次。如果您不介意,那么您为什么不介意第二次解析的性能,因为您不介意性能?我对此有点困惑。

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


【解决方案1】:

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

罪魁祸首是,对于一个代码单元的代码点,最高 0 位是第 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

请注意,bsr 不是为输入 0 定义的,但这只会发生在无效的前导代码单元(值为 11111111b)上。

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

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

【讨论】:

  • 好的,是的...我没有正确编码。因此,当我这样做时,第一个字节(如果不是直接的 ascii)将具有控制位,该控制位还可能编码位长度(基于 0)4-6(对于 2 到 4 字节计数)。是否有从 msb 中查找 0 位的指令?如果是这样,我可以找到,我可以切换第 7 位,按索引位向右移动,然后只使用该值作为计数将保留?
  • @FrankC。我找不到找到最高零位(MSb 中的第一个零)的指令。我通过先不输入代码单元并使用bsr 来解决。关于您的第二个问题,您需要考虑到最高零的位置存在“不连续性”(单个代码单元字符的索引 7,双代码单元字符的索引 5,后续索引 4 和更低)加长字符)
  • 同意并谢谢。我将您的标记为解决方案。
  • 而不是neg al + add al, 7 + setz al,为什么不只是cmp al, 14 + setz al
  • @CodyGray 我必须计算函数 f(i) 使得: f(7) -> 1, f(5) -> 2, f(4) -> 3, f(3 ) -> 4. 所以我做7-i == 0 ? 1 : 7-i。我没有遵循您提出的解决方案。顺便说一句,感谢您的编辑!
【解决方案2】:

如果您坚持颠倒字节顺序(无论出于什么奇怪的原因),您仍然可以简单地扫描设置为 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 代码。

对于以零字节结尾的无效 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错误的。但是第二个+字节不正确的正确前导位(如c3 00)仍将返回2,而第一个变体在这种情况下返回1

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


顺便说一句,有一些 i18n 库(开源)可以完成所有这些工作,例如验证 utf8 输入、修复无效输入、计数字符等......其中一些已经存在了十多年......然而仍然收到错误报告和修复。这是一个微妙的暗示,正确地编写这些东西是多么困难(不让应用程序暴露于某些输入数据漏洞)。 :)

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

还有一个离题的建议:如果你曾经尝试用 PHP 编写一些东西,应该处理 UTF8 输入数据(不是来自受信任的来源,但甚至来自受信任的来源),特别是如果这些输入数据来自 GET/发布回复......只是不要靠你自己。绝不。为那个获得一些框架。 :)

【讨论】:

  • 订单是我的错误。假设eax = 0x000089c3
  • @FrankC。然后我相信玛格丽特的解决方案也可以,但这个也应该适用于有效的 UTF-8 代码,因为 MSB 仍然不能为零,所以 some 位将在 MSB 中设置,“div 8" 无论如何都会得到正确的结果 [0,1,2,3]。 ...这可能会因无效的 UTF8 代码而失败(内部(最后)字节为零)。确保您的代码在这种情况下不可可被利用。
  • 关于漏洞利用的好(+1)点。我会增加更多的严格性。谢谢!
  • @FrankC.:顺便说一句,这当然只是因为您已经将字符解析为适当的长度。因此,如果您真的很喜欢性能,那么第一个解析器已经有了长度。就拿它吧。如果你在 eax 后面有 UTF-8 char + undefined mess,这将是有意义的,所以这将是处理 char 的第一个实例。但是代码必须不同。或者玛格丽特的一个(对于FF 来说是未定义的......所以......还有一些工作要做,如果你需要防弹:))。
  • 我没有更改“char”结构/对象元数据以包含长度的奢侈,这就是为什么我在一个地方进行解析以及稍后可能发生的使用点'未来(如印刷等)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-09
  • 2018-03-15
  • 2011-07-02
  • 1970-01-01
  • 2021-06-03
相关资源
最近更新 更多