我们能以多快的速度制作特定的tr?

时间:2015-06-10 20:42:21

标签: c performance io

我不得不用另一个字符替换文件中的所有空字节(我任意选择@),并且非常惊讶tr '\00' '@'大约是{{1}的1/4速度}:

gzip

我的真实数据文件是3GB gzip,花了50分钟到$ pv < lawl | gzip > /dev/null ^C13MiB 0:00:04 [28.5MiB/s] [====> ] 17% ETA 0:00:18 $ pv < lawl | tr '\00' '@' > /dev/null ^C58MiB 0:00:08 [7.28MiB/s] [==> ] 9% ETA 0:01:20 ,我实际上需要在许多这样的文件上执行此操作,因此它不是一个完全学术问题。请注意,从磁盘读取(这里速度相当快的SSD)或tr,在任何一种情况下都不是瓶颈; pvgzip都使用100%的CPU,tr速度更快:

cat

此代码:

$ pv < lawl | cat > /dev/null
 642MiB 0:00:00 [1.01GiB/s] [================================>] 100%
#include <stdio.h> int main() { int ch; while ((ch = getchar()) != EOF) { if (ch == '\00') { putchar('@'); } else { putchar(ch); } } } 编译的

有点快:

clang -O3

使用$ pv < lawl | ./stupidtr > /dev/null ^C52MiB 0:00:06 [ 8.5MiB/s] [=> ] 8% ETA 0:01:0 (4.8.4)进行编译具有可比性,可能会稍微快一些。将gcc -O4 -mtune=native -march=native添加到clang(-march=native)会生成相同的二进制文件。

这可能只是因为Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn)中替换的通用处理代码被常量替换,并且可以编译检查。 LLVM IR(tr)看起来很不错。

我猜clang -S -O3 stupidtr.c必须更快,因为它正在执行SIMD指令或其他操作。是否有可能达到gzip速度?

某些规格,如果它们相关:

  • 该文件为CSV;空字节只能出现在某个字段中,但其他一些字段是可变长度的,所以你不能随意搜索。大多数行在该字段中都有一个空字节。我想这意味着您可以gzip搜索,\00,,如果有帮助的话。一旦你找到了一个空字节,它也可以保证不会有一百个左右的字节。

  • 一个典型的文件大约是20 GiB未压缩,但如果相关,则在磁盘上压缩bz2。

  • 您可以根据需要进行并行化,但gzip会对其进行并行化,因此不一定非必要。我将在运行OSX的四核i7或运行Linux的两个vCPU云服务器上运行它。

  • 我可能运行的两台机器都有16GB的RAM。

4 个答案:

答案 0 :(得分:4)

您需要使用块读取和写入来提高速度。 (即使使用像stdio.h这样的缓冲I / O库,管理缓冲区的成本也很高。)类似于:

#include <unistd.h>
int main( void )
{
    char buffer[16384];
    int size, i;
    while ((size = read(0, buffer, sizeof buffer)) > 0) {
        for( i = 0; i < size; ++i ) {
            if (buffer[i] == '\0') {
                buffer[i] = '@';
                // optionally, i += 64; since
                // "Once you've found a null byte, it's also guaranteed that there can't
                // be another one for a hundred bytes or so"
            }
        }
        write(1, buffer, size);
    }
}

当然,使用优化进行编译,以便编译器可以将索引转换为指针算法(如果有用)。

如果你仍然没有达到你的速度目标(或者足够聪明的编译器可以自动向量化for循环),这个版本也很适合SIMD优化。

此外,此代码缺乏可靠的错误处理。正如@chqrlie在评论中提到的那样,当你得到-EINTR时应该重试,你应该处理部分写入。

答案 1 :(得分:4)

将各种答案中的想法与一些额外的比特结合起来,这是一个优化版本:

#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE  16384
#define REPLACE_CHAR  '@'

int main(void) {
    /* define buffer as uint64_t to force alignment */
    /* make it one slot longer to allow for loop guard */
    uint64_t buffer[BUFFER_SIZE/8 + 1];
    ssize_t size, chunk;
    uint64_t *p, *p_end;
    uint64_t rep8 = (uint8_t)REPLACE_CHAR * 0x0101010101010101ULL;

    while ((size = read(0, buffer, BUFFER_SIZE)) != 0) {
        if (size < 0) {
            if (errno == EINTR) continue;
            fprintf(stderr, "read error: %s\n", strerror(errno));
            return 1;
        }
        p = buffer;
        p_end = p + ((size + 7) >> 3);
        *p_end = 0ULL; /* force a 0 at the end */
        for (;; p++) {
#define LOWBITS   0x0101010101010101ULL
#define HIGHBITS  0x8080808080808080ULL
            uint64_t m = ((*p - LOWBITS) & ~*p & HIGHBITS);
            if (m != 0) {
                if (p >= p_end) break;
                m |= m >> 1;
                m |= m >> 2;
                m |= m >> 4;
                *p |= m & rep8;
            }
        }
        for (unsigned char *pc = (unsigned char *)buffer;
             (chunk = write(1, pc, (size_t)size)) != size;
             pc += chunk, size -= chunk) {
            if (chunk < 0) {
                if (errno == EINTR) continue;
                fprintf(stderr, "write error: %s\n", strerror(errno));
                return 2;
            }
        }
    }
    return 0;
}

答案 2 :(得分:3)

您的代码不正确,因为您没有在正确的位置测试文件结尾。这是do {} while循环中的非常常见错误。我建议完全避免使用这个结构(除非在宏中将语句序列转换为单个语句)。

还试着告诉glibc对流进行较少的检查:

#include <stdio.h>

int main() {
    int c;
    while ((c = getchar_unlocked()) != EOF) {
        if (c == '\0')
            c = '@':
        putchar_unlocked(c);
    }
}

您也可以使用不同的缓冲区大小,例如在while()循环之前尝试这些:

setvbuf(stdin, NULL, _IOFBF, 1024 * 1024);
setvbuf(stdout, NULL, _IOFBF, 1024 * 1024);

如果将该实用程序用作带管道的过滤器,则不应产生太大影响,但如果使用文件,它可能会更有效。

如果您使用文件,您还可以mmap该文件并使用memchr查找可能更快的'\0'字节,甚至strchr,您可以确保在文件的末尾有一个'\ 0``(把它放在一个好方法)。

答案 3 :(得分:0)

首先,正如其他人所说,不要使用getchar()/putchar(),甚至不使用任何基于文件的方法,例如fopen()/fread()/fwrite()。请改用open()/read()/write()

如果文件已在磁盘上解压缩,请不要使用管道。如果它被压缩,那么你想要使用管道来删除整个读/写周期。如果从磁盘解压缩到磁盘,则替换NUL字符,数据路径为disk-&gt; memory / cpu-&gt; disk-&gt; memory / cpu-&gt; disk。如果使用管道,则路径为disk-&gt; memory / cpu-&gt; disk。如果您受磁盘限制,那么额外的读/写周期将大约是处理您的千兆字节(或更多)数据所需时间的两倍。

还有一件事 - 考虑到您的IO模式和移动的数据量 - 读取整个多GB文件,写入整个文件 - 页面缓存只会妨碍您。使用直接IO,因此在Linux上使用C(标题和强大的错误检查,为清晰起见):

#define CHUNK_SIZE ( 1024UL * 1024UL * 4UL )
#define NEW_CHAR '@'
int main( int argc, char **argv )
{
    /* page-aligned buffer */
    char *buf = valloc( CHUNK_SIZE );
    /* just set "in = 0" to read a stdin pipe */
    int in = open( argv[ 1 ], O_RDONLY | O_DIRECT );
    int out = open( argv[ 2 ], O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT, 0644 );

    for ( ;; )
    {
        ssize_t bytes = read( in, buf, CHUNK_SIZE );
        if ( bytes < 0 )
        {
            if ( errno == EINTR )
            {
                continue;
            }
            break;
        }
        else if ( bytes == 0 )
        {
            break;
        }

        for ( int ii = 0; ii < bytes; ii++ )
        {
            if ( !buf[ ii ] )
            {
                buf[ ii ] = NEW_CHAR;
            }
        }
        write( out, buf, bytes );
    }

    close( in );
    close( out );

    return( 0 );
}

将编译器优化尽可能高。要在真实数据上使用此代码,您需要检查write()调用的结果 - Linux上的直接IO是一个真正挑剔的野兽。我必须关闭一个用O_DIRECT打开的文件,并在没有直接IO设置的情况下重新打开它,以便在最后一位不是整页的多个时写入Linux上文件的最后一个字节。

如果你想更快,你可以多线程处理 - 一个线程读取,一个线程转换字符,另一个线程写入。使用尽可能多的缓冲区,根据需要将它们从线程传递给线程,以保持进程中最慢的部分始终处于忙碌状态。

如果您真的对看到移动数据的速度感兴趣,那么多线程读/写也是如此。如果您的文件系统支持它,请使用异步读/写。