我如何摆脱 RISC-V 中的 '\n'?

How do I get rid of '\n' in RISC-V?

我的任务是创建一个程序,该程序将读取文件名,然后将其内部复制到其他文件,该文件的名称也从输入中读取。我自己写了程序,但它似乎什么也没做。

进一步试验,我发现,在读取第一个字符串时,程序还会在其中保存一个 '\n' 字符,这显然会导致搜索目标文件时出现一些问题。我想出了一个解决方案,但我并不完全喜欢,这就是为什么我在这里征求对代码和整体进一步改进的意见,也许吧?

我只固定了负责将文件名写入缓冲区的部分,直到 '\n' 出现。

    .text
main:
#first block
    sbrk(128)
    mv s3, a0
    li a7, 8
    li a1, 127
    ecall
    
for:
    lw t0, 0(a0)
    li s1, 0x000000ff
    li s2, 0x0000000a
    
ff_and:
    and t1, t0, s1
    addi s4, s4, 1
    beq t1, s2, kill
    slli s1, s1, 8
    slli s2, s2, 8
    bnez s1, ff_and
    
    addi a0, a0, 4
    b for
    
kill:
    neg s1, s1
    addi s1, s1, -1
    and t0, t0, s1
    sw t0, 0(a0)

这很不错,因为它很管用!评论:

  • 因为它处理字大小的字符串——一次 4 个字符的块——它可以说比其他一次处理 1 个字节的方法更复杂。

  • 一次处理 4 个字节,它会忽略所讨论的字符串是否从单词对齐的边界开始——虽然在这个程序的情况下是这样,但对于一般的。未对齐的字大小加载和存储会受到某些危害的影响,并且在某种程度上取决于底层处理器,范围从性能问题到访问错误。固定算法以适应任意对齐,同时仍一次处理 4 个字节,将增加相当大的复杂性。

  • 一次处理 4 个字节,它可能会读过字符串末尾,进入另一个数据结构。虽然这不太可能在给出字对齐字符串时引起任何问题,但在读取数据结构末尾时通常会皱眉,如果处理器支持未对齐加载,这会产生读取到下一个缓存行的不良影响或页面不是字符串本身的一部分。

一行终端输入包含终止换行符是正常的。如果 RARS 不允许用户在没有换行符的情况下“提交”输入,您可以将最后一个字节归零。但是 RARS 读取字符串 ecall 非常不方便 return 长度,所以搜索 [=12=] 并不比只搜索 \n.

(一个 Unix read 系统调用 return 一个长度:RARS 将其作为 ecall #63 read 而 return 是一个a0 中的长度,因此如果它允许标准输入的 fd=0,您可以使用它来读取输入。)


循环效率

你每次循环迭代只做一个字节;您唯一节省的是每次迭代 (lb) 的字节加载,但要付出更多的 ALU 工作。

简单的方法看起来像这样,并且在大多数真实世界的 RISC-V 机器上可能更快。 (特别是如果它们有任何缓存,这使得执行多个附近的负载而不是一个更宽的负载更便宜。)展开一些以隐藏负载延迟对于高性能有序机器来说可能是一个好主意,如果你真的关心优化这个循环用于潜在的大输入。 (对于这个用例,你不应该这样做,因为它只在每个用户输入时运行一次,所以只要保持代码大小紧凑即可。)

    li  t1, '\n'
 .loop:                     # do{
    lbu   t0, (a0)
    addi  a0, a0, 1
    bne   t0, t1, loop      # }while(*p != '\n')
                 # assume the string will *always* contain a newline,
                 #  otherwise check for 0 as well
    sb    zero, -1(a0)

    # a0 points to one-past-the-end of the terminating 0
    # so if you want the string length, you can get it by subtracting

但是关于一次一个字循环的设计选择还有更多要说的:

由于RISC-V有字节存储指令,所以不需要屏蔽掉换行处的单词,存储整个单词,只需要在换行处sb x0, (position) ,即使您通过为每个内循环移位计数递增计数器来找到该位置(这也应该简化该循环)。

此外,如果您的缓冲区不是对齐单词的整数,则存储整个单词尤其糟糕:您不想在缓冲区末尾之后执行非原子 RMW 字节。对于线程安全来说,这是一个非常糟糕的习惯。 (另请参阅 Erik 的回答:一般情况下一次一个字的可能缺点,以及

(如果您要屏蔽一个词并存储它,请使用 not 而不是 neg / addi -1 来反转您的屏蔽中的位。notxori-1 的伪指令。通常,您可以向编译器询问类似的东西,例如 https://godbolt.org/z/EPGYGosKd 显示 clang 如何为 RISC-V 实现 x & ~mask .)


一次一个字快速

实际上快速一次检查整个单词的换行字节,执行word ^ 0x0a0a0a0a将该字节值映射到0,并且其他值设置为非零。 然后使用 bithack 查找单词是否具有零字节 https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord. (Like what glibc's portable-C fallback strlen does: )。 IIRC,这不是一个精确的测试(可能出现误报匹配),所以你想快速检查一个完整的单词,然后循环检查一个字节,一次检查一个字节以确保。如果none,回到单词循环。

当然,如果您有一些 SIMD 支持并行进行 4 或 8(或 16)字节比较,使用 RV32 P(打包 SIMD)或 RV32 V(矢量)扩展,那就更好了。

如果您在未分配的缓冲区上执行此操作,您可能想要执行一次未对齐加载(在 之后),然后到达对齐字加载的对齐边界。或者一次一个字节地循环直到一个字边界。 (或 RV64 上的双字)。