【问题标题】:How do I get rid of '\n' in RISC-V?如何摆脱 RISC-V 中的“\n”?
【发布时间】:2023-03-19 16:29:01
【问题描述】:

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

进一步实验,我发现,在读取第一个字符串时,程序还会在其中保存一个'\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)

【问题讨论】:

  • 一行终端输入包含终止换行符是正常的。如果 RARS 不允许用户在没有换行符的情况下“提交”输入,您可以将最后一个字节归零。但是 RARS 读取字符串 ecall 愚蠢地不返回长度,所以搜索 \0 并不比只搜索 \n 更好。

标签: string assembly io riscv rars-simulator


【解决方案1】:

一行终端输入包含终止换行符是正常的。如果 RARS 不允许用户在没有换行符的情况下“提交”输入,您可以将最后一个字节归零。但是 RARS 读取字符串 ecall 非常不方便地不返回长度,因此搜索 \0 并不比仅搜索 \n 更好。

(Unix read 系统调用返回一个长度:RARS 有它作为ecall #63 read 它返回一个在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 的回答:一般来说一次单词的可能缺点和Is it safe to read past the end of a buffer within the same page on x86 and x64?

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


一次快速的单词

要真正快速一次检查整个单词的换行字节,请执行 word ^ 0x0a0a0a0a 将该字节值映射为 0,并将其他值映射为非零。 strong> 然后使用 bithack 来查找单词是否具有零字节 https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord。 (就像 glibc 的可移植 C 后备 strlen 所做的那样:Why does glibc's strlen need to be so complicated to run quickly?)。 IIRC,这不是一个精确的测试(可能存在误报匹配),因此您需要快速检查整个单词,然后遍历字节一次检查一个以确保。如果没有,请返回单词循环。

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

如果您在未分配的缓冲区上执行此操作,您可能希望执行一次未对齐的加载(在checking it's not going to cross a page or maybe cache-line boundary 之后),然后到达对齐边界以进行对齐的字加载。或者一次循环一个字节,直到一个字边界。 (或 RV64 上的双字)。

【讨论】:

  • “这也应该是那个循环”应该读作简化
  • @ecm:谢谢,已修复。
【解决方案2】:

这很好,因为它有效!评论:

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

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

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

【讨论】:

  • 如果您使用对齐的负载(例如,在到达可能未对齐的缓冲区开始附近的对齐边界之后),读取过去结束的问题就会消失。您只会读取一个包含至少一个字符串字节的字大小的块,并且对齐的负载不能跨两个缓存行拆分。 Is it safe to read past the end of a buffer within the same page on x86 and x64? 有详细信息,还建议检查您是否距离页面末尾足够远,可以加载前 16(或 4)字节。
  • 但是,lb 会更容易,并且还可以避免对象末尾的非原子 RMW 字节。 对线程安全来说是一个很大的禁忌,不像只是阅读单词。
  • RISC V 硬件通常会在未对齐访问时出现故障,但我们可以设想可能模拟未对齐访问的软件环境,在这种情况下,代码看起来可以正常工作,但会遇到严重的性能问题(更不用说其他问题)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-06-14
  • 2015-10-03
  • 1970-01-01
  • 2021-05-19
  • 1970-01-01
  • 2023-04-01
  • 1970-01-01
相关资源
最近更新 更多