2

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

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

2 回答 2

2

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

(Unixread系统调用返回一个长度:RARS 将其作为ecall#63read返回一个长度 in 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)找到换行符的位置,即使您通过递增 a 找到该位置每个内循环移位计数的计数器(这也应该简化该循环)。

此外,如果您的缓冲区不是对齐单词的整数,那么存储整个单词尤其糟糕:您不想在缓冲区末尾执行非原子 RMW 字节。这对于线程安全来说是一个非常糟糕的习惯。(另请参阅 Erik 的回答:一般来说,一次一个单词的可能缺点,以及在 x86 和 x64 的同一页面内读取缓冲区末尾是否安全?

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


一次快速的单词

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

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

如果您在未分配的缓冲区上执行此操作,您可能想要执行一次未对齐的加载(在检查它不会跨越页面或缓存行边界之后),然后到达对齐边界对齐的单词加载。或者循环一个字节直到一个字边界。(或 RV64 上的双字)。

于 2021-03-31T17:00:09.627 回答
2

这很好,因为它有效!注释:

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

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

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

于 2021-03-31T16:14:49.460 回答