【问题标题】:How to access a char array and change lower case letters to upper case, and vice versa如何访问 char 数组并将小写字母更改为大写,反之亦然
【发布时间】:2016-06-26 05:30:00
【问题描述】:

我目前正在使用 x86 处理器进行结构化计算机组织的课程项目。我正在访问的值是一个 1 字节字符,但我不知道如何将它与大写进行比较。他们说要使用十六进制格式的 ASCII 表,但我不知道如何比较两者。

void changeCase (char char_array[], int array_size ) {
    __asm {
            // BEGIN YOUR CODE HERE
 
        mov eax, char_array;        //eax is base image
        mov edi, 0;
        
    readArray:
        cmp edi, array_size;
        jge  exit;
        mov ebx, edi;           //using ebx as offset
        shl ebx, 2;
        mov cl, [eax + ebx];    //using ecx to be the storage register
    
    check:
        //working on it
        cmp cl, 0x41;       //check if cl is <= than ASCII value 65 (A)
        jl next_indx;
        cmp cl, 0x7A;       //check if cl is >= than ASCII value 122 (z)
        jg next_indx;
        cmp cl, 'a';
        jl convert_down;
        jge convert_up;
        

    convert_down:
        or cl, 0x20;        //make it lowercase
        jmp write;

    convert_up:
        and cl, 0x20;       //make it uppercase
        jmp write;

    write:
        mov byte ptr [eax + ebx], cl    //slight funky town issue here,

    next_indx:
        inc edi;

    exit:
        cmp edi, array_size;
        jl readArray;

    mov char_array, eax;
            // END YOUR CODE HERE
    }
}

此时任何事情都会有所帮助。提前感谢您的帮助!

编辑1:

感谢所有建议和清晰的要点,编辑我的代码以反映变化。现在访问冲突有些问题。

编辑 2 (+):

感谢帮助眼睛的人。我现在还在翻译所有的信件。

【问题讨论】:

  • 据我了解,MSVC 为您执行推送/弹出操作,以保存/恢复您使用的任何寄存器。如果您查看反汇编输出,您的 push/pop 指令可能是多余的。直接在 asm 中编写函数,而不是在 C 函数中内联 asm,这意味着您必须了解调用约定,但是一旦成功,您就会更好地了解发生了什么。
  • 您好彼得,感谢您的意见。我将很快处理调用者/被调用者功能。我无法更改注释行之外的代码。
  • 如果您查看ascii table,您应该会注意到大写字符的值范围是连续的,并且与小写字符的值范围是分开的。这个事实应该会有所帮助。
  • 你真的应该学会用调试器逐步调试,它会让你更容易看到最终的问题。您的 convert_up 和 convert_down 代码不正确,我不确定您为什么在最后使用 mov char_array, eax; 丢弃数组(似乎应该删除该行)。

标签: assembly x86 ascii


【解决方案1】:

这个问题的变体一直被问到。这个版本的问题(需要if(isalpha(c)) c|=0x20; 之外的条件行为)使问题变得足够复杂,以至于如何有效地解决问题并不是很明显。

事实证明,xor 并不难想到,并且将此代码无条件地转换为大写或小写只需要从 xor 0x20and ~0x20or 0x20 的简单更改。 (也可以再简化一点。)

这就是我会尝试最佳高效汇编的方式。我什至包含了一个带有 SIMD 向量的版本,以及另一个版本的字节循环,它使用了我从向量化中得到的无分支想法。

只有在您了解了使用未优化代码解决此问题所涉及的基本原则后,阅读此答案可能才有用。 OTOH,实际需要的操作很少,所以没有太多代码可以理解。我确实对它进行了大量评论。 标签 wiki 中有许多有用的链接,从教程到参考指南再到性能调整。


小写和大写字母 ASCII 字符之间的转换只需要设置或清除 0x20 位,因为 ASCII 字符集的布局是彼此相距 32 的范围,并且不跨越 mod32 边界。

对于每个字节:

  • 制作一个副本并无条件地用 0x20 或它
  • 检查它是否在'a''z'之间
  • 如果是这样,使用 xor 翻转 ASCII 字母大小写位并将结果存储回数组中。

以这种方式进行 ASCII isalpha(3) 测试是安全的:从设置该位到 'a'..'z' 范围内的唯一源字节是大写字母字符。这只是适用于不跨越%32 边界的任何两个大小相等的范围的数学。 (例如,如果相关位为 0x40,则为 %64 边界)。

为了更有效地进行比较,我使用了无符号比较技巧,因此循环内只有一个条件分支(循环条件本身除外)。解释见代码中的 cmets。

一次一个字节,对字母字符检测进行有效的范围检查

/******** Untested. ************/

// ASCII characters are flipped to the opposite case (upper <-> lower)
// non-ASCII characters are left unchanged
void changeCase (char char_array[], int array_size ) {

    __asm{
            // BEGIN YOUR CODE HERE

        mov   esi, char_array;      // MSVC inline asm requires these potentially-redundant copies :(
        mov   ecx, array_size;

        test  ecx,ecx;       // return if(size <= 0)
        jle  early_out;

    next_char:
        movzx eax, byte ptr [esi];     // load the current character
        mov   edx, eax;              // save a copy to maybe flip + store

        // check if the character is alphabetic or not
        // there are two equal-size ranges of characters: one with 0x20 set, and one without
        or    al, 0x20;      // set 0x20 and then just check that lowercase range

        // unsigned compare trick: 0 <= n < high  can be done with one unsigned compare instead of two signed compares
        // low < n < high  can be done by shifting the range first
        sub   al, 'a';       // if al is less than 'a', it will become a large unsigned number
        cmp   al, 'z'-'a';
        ja  non_alpha;      // conditionally skip the flip & store

        xor   dl, 0x20;      // toggle the ASCII case bit
        mov   [esi], dl;
                             // xor [esi], 0x20   // saves the mov earlier, but is otherwise slower
    non_alpha:

        inc   esi;
        dec   ecx;
        jz next_char;

    early_out:
            // END YOUR CODE HERE
    }
}

如果某些“设计文档”内容位于代码之外的块中,则此代码可能更具可读性。它把事情弄得乱七八糟,看起来有很多代码,但实际上指令很少。 (它们只是很难用简短的 cmets 来解释。注释代码很棘手:过于明显的 cmets 只会造成混乱,并且会占用阅读代码和有用 cmets 的时间。)


矢量化

实际上对于 x86,我会使用 SSE 或 AVX 一次执行 16B,执行相同的算法,但与两个 pcmpgtb 进行比较。当然,无条件地存储结果,所以所有非字母字符的数组仍然会在缓存中被弄脏,使用更多的内存带宽。

没有无符号的 SSE 比较,但我们仍然可以将我们正在寻找的范围向下移动到底部。没有小于-128 的值,因此在有符号比较中它的工作方式与0 在无符号比较中的工作方式相同。

为此,减去128(or add, or xor (carryless add); there's nowhere for the carry / borrow to go)。这可以在与减去'a' 相同的操作中完成。

然后使用比较结果作为掩码将0x20 向量中的字节清零,因此只有字母字符与0x20 进行异或。 (0 是 XOR/add/sub 的标识元素,这对于 SIMD 条件通常非常方便)。

另请参阅strtoupper version that has been tested在循环中调用它的代码,包括在隐式长度 C 字符串上处理非 16 倍数的输入(搜索终止的 0在飞行中)。

#include <immintrin.h>

// Call this function in a loop, with scalar cleanup.  (Not implemented, since it's the same as any other vector loop.)

// Flip the case of all alphabetic ASCII bytes in src
__m128i inline flipcase(__m128i src) {
    // subtract 'a'+128, so the alphabetic characters range from -128 to -128+25 (-128+'z'-'a')
    // note that adding 128 and subtracting 128 are the same thing for 8bit integers.
    // There's nowhere for the carry to go, so it's just xor (carryless add), flipping the high bit

    __m128i lcase = _mm_or_si128(src, _mm_set1_epi8(0x20));

    __m128i rangeshift= _mm_sub_epi8(lcase, _mm_set1_epi8('a'+128));
    __m128i non_alpha = _mm_cmpgt_epi8(rangeshift, _mm_set1_epi8(-128 + 25));  // 0:alphabetic   -1:non-alphabetic

    __m128i flip  = _mm_andnot_si128(non_alpha, _mm_set1_epi8(0x20));       // 0x20:alpha    0:non-alpha

    return          _mm_xor_si128(src, flip);
    // just mask the XOR-mask so non-alphabetic elements are XORed with 0 instead of 0x20
    // XOR's identity value is 0, same as for addition
}

这个compiles to nice code, even without AVX,只有一个额外的movdqa 来保存一个寄存器的副本。查看两个早期版本的 godbolt 链接(一个使用两个比较以保持简单,另一个使用 pblendvb,然后我记得屏蔽 0x20s 的向量而不是结果。)

flipcase:
        movdqa  xmm2, XMMWORD PTR .LC0[rip]    ; 0x20
        movdqa  xmm1, xmm0
        por     xmm1, xmm2
        psubb   xmm1, XMMWORD PTR .LC1[rip]    ; -31
        pcmpgtb xmm1, XMMWORD PTR .LC2[rip]    ; -103
        pandn   xmm1, xmm2
        pxor    xmm0, xmm1
        ret

section .rodata
    .LC0:   times 16 db  32
    .LC1:   times 16 db  -31
    .LC2:   times 16 db  -103

使用无分支测试的同样想法也适用于字节循环:

        mov   esi, char_array;
        mov   ecx, array_size;

        test  ecx,ecx;       // return if(size <= 0)
        jle  .early_out;

    ALIGN 16   ; really only need align 8 here, since the next 4 instructions are all 2 bytes each (because op  al, imm8  insns have a special encoding)
    .next_char:
        movzx  eax, byte ptr [esi];     // load the current character
        mov    edx, eax;

        // check if the character is alphabetic or not
        or    al, 0x20;        
        sub   al, 'a';
        cmp   al, 'z'-'a';   // unsigned compare trick: 'a' <= al <= 'z'
        setna al;            // 0:non-alpha  1:alpha  (not above)
        shl   al, 5;         // 0:non-alpha  0x20:alpha
        xor   dl, al;        // conditionally toggle the ASCII case bit
        mov   [esi], dl;     // unconditionally store

        inc   esi;
        dec   ecx;           // for AMD CPUs, or older Intel, it would be better to compare esi against an end pointer, since cmp/jz can fuse but dec can't.  This saves an add ecx, esi outside the loop
        jz .next_char;
    .early_out:

对于 64 位代码,只需使用 rsi 而不是 esi。其他一切都一样。

显然是MSVC inline asm doesn't allow .label local-symbol names。我将它们更改为第一个版本(带有条件分支),但不是这个。

使用movzx eax, byte [esi] 优于mov al, [esi],避免了对AMD、Intel Haswell 及更高版本以及Silvermont 系列的错误依赖。 movzx 并不像旧 AMD 的负载那么便宜。 (它至少在 Intel 和 AMD Ryzen 上,一个只使用加载端口,而不是 ALU 端口的微指令)。 Why doesn't GCC use partial registers?

之后在al 上运行仍然可以。没有partial-register stall(或避免它的额外说明),因为在setcc 写入al 之后我们没有读取eax。 (可惜没有setcc r/m32,只有r/m8)。


我想知道如果有人为这样的任务提交这样的代码,教授会怎么想。 :P 我怀疑即使是智能编译器也会使用 setcc / shift 技巧,除非您将编译器引向它。 (可能是unsigned mask = (tmp&gt;='a' &amp;&amp; tmp&lt;='z'); mask &lt;&lt;= 5; a[i] ^= mask; 或其他东西。)编译器确实知道无符号比较技巧,但gcc doesn't use it in some cases for non-compile-time-constant range checks, even when it can prove that the range is small enough

【讨论】:

  • 不错的一个 :) 但这个解决方案也有一个问题,即 'Z' 和 'a' 之间的字符被视为有效字符......哦,等一下,我用 int 检查它,而不是 unsigned C中的int ...我的错。所以是的,不错的“hack”
  • 我在 C 中尝试过类似的东西,结果大多数是 -(200+x),而 ']' 是 28 ...并且没有考虑“> 26”仍然是 TRUE对于汇编程序中的那些 -200 值(字节环绕)。太糟糕了,方向很好:)
  • @Tommylee2k:是的,很难理解。你看到sub reg, 'a',然后是cmp reg, 25,觉得“cmp也是减法,为什么不能合并?”但关键是起点对于设置标志(进位和溢出)很重要。这不仅仅是测试结果的符号位。
  • 是的,如果你将一个范围“拖动”到“零”,范围检查所需要做的就是检查上边界...“a”
  • @Tommylee2k:我为自己提出了将范围“拖动”到 -128 的想法感到非常自豪,因此我可以使用 pcmpgtb 对其进行矢量化。我没有自己提出无符号比较,但我确实(重新?)发明了它与pcmpgtb 的使用。大多数情况下,减少分支数量是一种性能问题,尤其是。取了树枝。正确性仍然不是微不足道的,因为您必须确保在减法中没有不合一的情况(下限
【解决方案2】:

为了清楚起见,我将只使用纯汇编并假设...

  • char_array[ebp+8] 的 32 位指针。
  • array_size[ebp+12] 处的二进制补码 32 位数字。
  • 对于您的平台(反正大多数情况都是这样),char 的编码是 ASCII。

您应该能够自己将其推断为内联汇编。现在,如果您查看the table everyone is supposed to remember but barely anyone does,您会注意到一些重要的细节...

  • 大写字母 AZ 分别映射为代码 0x410x5A
  • 小写字母az 分别映射为代码0x610x7A
  • 其他都不是字母,因此不需要大小写转换。
  • 如果您查看大写和小写字母范围的二进制表示,您会注意到它们完全相同,唯一的例外是大写字母清除了第 6 位,而小写字母则设置了它。李>

因此,算法将是……

while array_size != 0
    byte = *char_array
    if byte >= 0x41 and byte <= 0x5A
        *char_array |= 0x20 // Turn it lowercase
    else if byte >= 0x61 and byte <= 0x7A
        *char_array &= 0xDF // Turn it uppercase
    array_size -= 1
    char_array += 1

现在,让我们把它翻译成汇编...

mov eax, [ebp+8]      # char *eax = char_array
mov ecx, [ebp+12]     # int ecx = array_size

.loop:
    or ecx, ecx       # Compare ecx against itself
    jz .end_loop      # If ecx (array_size) is zero, we're done
    mov dl, [eax]     # Otherwise, store the byte at *eax (*char_array) into `char dl`
    cmp dl, 'A'       # Compare dl (*char_array) against 'A' (lower bound of uppercase letters)
    jb .continue      # If dl` (*char_array) is lesser than `A`, continue the loop
    cmp dl, 'Z'       # Compare dl (*char_array) against 'Z' (upper bound of uppercase letters)
    jbe .is_uppercase # If dl (*char_array) is lesser or equal to 'Z', then jump to .is_uppercase
    cmp dl, 'a'       # Compare dl (*char_array) against 'a' (lower bound of lowercase letters)
    jb .continue      # If dl (*char_array) is lesser than 'a', continue the loop
    cmp dl, 'z'       # Compare dl (*char_array) against 'z' (upper bound of lowercase letters)
    jbe .is_lowercase # If dl (*char_array) is lesser or equal to 'z', then jump to .is_lowercase
    jmp .continue     # All tests failed, so continue the loop

    .is_uppercase:
        or dl, 20h    # Set the 6th bit
        mov [eax], dl # Send the byte back to where it came from
        jmp .continue # Continue the loop

    .is_lowercase:
        and dl, DFh   # Clear the 6th bit
        mov [eax], dl # Send the byte back to where it came from
        jmp .continue # Continue the loop

    .continue:
        inc eax       # Increment `eax` (`char_array`), much of like a pointer increment
        dec ecx       # Decrement `ecx` (`array_size`), so as to match the previous pointer increment
        jmp .loop     # Continue

.end_loop:

一旦代码到达.end_loop,就完成了。

我希望这对你有所启发!

【讨论】:

  • 谢谢!这一步一步的过程正是我所需要的!我会接受我所做的并很快上传结果。如果我能,呃,给你投票。
  • @archon263:如果您还不能投票,请不要担心;)。如果您想内联相同的代码,只需将[ebp+8] 替换为[char_array] 并将[ebp+12] 替换为[array_size]
  • 我不记得 ascii 表。我只是在源代码(包括 asm 源代码)中使用'a''0' 之类的常量,并在需要时运行ascii(1) 程序在我的终端中打印表格。您可以用'A''Z''a''z' 替换您的魔法十六进制常量。您还可以简化比较:将or0x20 复制一份,然后您只需要在'a''z' 之间进行检查。您可以使用无符号比较技巧进行范围检查:减去'a'cmp al, 26; ja .non_digit。在al&lt;'a' 的情况下,子环绕,产生一个大(无符号)数。
  • 如果解释行是 cmets,而不是交替代码和非代码,这将更容易阅读。至少我是这么认为的,也许对于初学者来说更容易阅读?通过寻找罕见的非缩进行,很难以通常的方式在视觉上找到分支目标。您的几乎所有解释都可以轻松放在评论行中。
  • 现在它是可读的(+1 表示):jmp .continue.continue: 之前的那一行是无操作的,应该被删除。此外,您可以反转.is_lowercase.is_uppercase,因此四个条件分支中的最后一个可以是jnbe .continue,否则会落入.is_lowercase。另外test ecx,ecx 总是比or ecx,ecx 好,因为它可以与jcc 进行宏融合。
【解决方案3】:

在 ASCII 中,'a'-'z' 和 'A'-'Z' 是等价的,除了一个位 0x20

你的朋友是 XOR。

如果你有一个字符('A'-'Z' 或 'a'-'z'),用 0x20 异或会切换大小写;

在异或之前,进行范围检查是有意义的。 (看看这个值是否真的是一个字母)
你可以简化这个范围检查,通过 ORing 值来检查 0xef,这将使 'a' 到 'A' 和 'z' 到 'Z',然后只做一次范围检查
(如果你只比较'Z'你会错过中间的字符('[',']'等......)

【讨论】:

  • 很好,我还想过使用or 来简化范围检查。我不确定它有多明显或容易理解,所以我花了很多时间解释它,因为我担心人们会想知道为什么在你还不知道它是字母字符的情况下做 tolower 是安全的。我很高兴其他人也想到了这一点。我认为编写优化的实现会很有趣,请参阅我的答案。我使用了另一个你没有提到的技巧(无符号比较技巧)。
【解决方案4】:

感谢@KemyLand 对汇编代码的帮助,我已经弄清楚如何将大写字母转换为小写字母,反之亦然。

void changeCase (char char_array[], int array_size ) {
     //this function is designed to change lowercase letters to uppercase, and vice-versa, from a char-array given the array and its size.
__asm{
        // BEGIN YOUR CODE HERE

    mov eax, [ebp + 8];     //move to register value parameter 1 (the array)
    mov ecx, [ebp + 12];    //likewise parameter 2 (the array size)

    START:

        or ecx, ecx;    //check if pointer is 0
        cmp ecx, 0;
        je endloop;   //go to end loop

        mov dl,byte ptr [eax];  //not sure if needed, but reassurance
        cmp dl, 0x41;   // is char an A?
        jl cont;

        cmp dl, 0x5A;   // is char a Z?
        jle convertUP;

        cmp dl, 0x61;   // is char an a?
        jl cont;

        cmp dl, 0x7A;   // is char a z?
        jle convertDOWN;

        jmp cont;


    convertUP:
        or dl, 0x20;        //Yes! Finally got it working!
        mov byte ptr [eax], dl;

        jmp cont;

    convertDOWN:
        and dl, 0xdf;       //this will work for sure.
        mov[eax], dl;

        jmp cont


    cont:
        inc eax;
        dec ecx;

        jmp START;

    endloop:
}

}

请随时帮助解释我可能错过的内容!感谢大家帮助我更好地了解 x86 汇编处理器。

【讨论】:

  • 你可以写你的常量比如'a',而不是十六进制。那么你不需要注释来解释常量。此外,is char a z? 没有正确描述cmp / jle。 “是一个”听起来更像cmp / je。代码是对的,注释是错误的。有一种说法是“asm 代码只有两种 bug:1. 代码与 cmets 不匹配。2. cmets 没有描述正确的算法”
  • 使用test ecx,ecx,而不是or ecx,ecx,因为它更快。将条件分支放在循环的底部,就像do{}while() 循环一样。构建你的分支以尽量减少跳跃。例如你应该能够安排事情,所以convertUP 之前的最后一个分支要么落入convertUP,要么跳转到cont。您甚至在cont: 之前有一个jmp cont,它...跳过了源代码中的空白?? :P.
  • mov eax, [ebp + 8]; 这样的东西是内联汇编中的主要禁忌。您的函数可以很容易地内联到另一个函数中,或者在没有帧指针的情况下编译。幸运的是,您不必假设您的 args 在堆栈中的位置,您可以通过写 mov eax, char_array 告诉 MSVC 将它们提供给您。这可能会变成多余的mov eax, esi 或其他东西; IDK,我没有看过 MSVC 输出。 AFAIK 没有办法只要求 MSVC 为您将变量放入寄存器中,并告诉它您的结果在哪些 regs 中(以避免存储和编译器重新加载)。
  • 您可以通过使用al 来保存您的源字节,在多个指令中保存一个字节的代码大小:cmp al, imm8or al, imm8 等有一个特殊的编码。别担心不过,这个。小代码量很好,但是在学习编写甚至可以在第一时间工作的代码时,还有更重要的事情需要考虑:P
  • 查看我的答案,了解不太明显的更重要的优化。我的整个循环是 11 条指令(包括循环开销),除了循环条件之外还有一个条件分支。玩得开心:D(我的意思是字面意思;我认为它是可以理解的并且评论很好。)由于这是一项任务,我认为您最好交出您在此答案中发布的内容。删除完全不需要的jmpor ecx,ecx,因为您使用cmp ecx,0 跟随它。 (test ecx,ecx 而不是 cmp 与 0 主要只是代码大小的胜利)。
【解决方案5】:

在 ascii 表中,所有字母都是连续的:

A=0x41=01000001
a=0x61=01100001

Z=0x5A=01011010
z=0x7A=01111010

因此,您可以看到,通过切换第 6 位,您可以将形式从大写转换为小写。

【讨论】:

    猜你喜欢
    • 2020-07-24
    • 2020-12-29
    • 1970-01-01
    • 1970-01-01
    • 2022-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多