我为类编写了这个汉明编码代码。为啥这么慢?

Posted

技术标签:

【中文标题】我为类编写了这个汉明编码代码。为啥这么慢?【英文标题】:I wrote this Hamming Encoding code for class. Why is it so slow?我为类编写了这个汉明编码代码。为什么这么慢? 【发布时间】:2014-05-01 06:38:00 【问题描述】:

我为我的操作系统课写了这个:

#include <iostream>
#include <fstream>

//encodes a file using the (8,4) Hamming Code.
//usage : HammingEncode.out < inputFile > outputFile 
int main() 
    unsigned char const codebook[] = 0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78, 0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF;
    unsigned char in, nextByte;
    unsigned char const leftMask = 0xF0, rightMask = 0x0F;

    in = std::cin.get();
    while (!std::cin.eof()) 
        nextByte = (in & leftMask) >> 4;
        std::cout << codebook[nextByte];
        nextByte = in & rightMask;
        std::cout << codebook[nextByte];
        in = std::cin.get();
    

然后我决定在詹姆士国王圣经中的旧约中测试它的速度(只是为了看看)。这是我们用 Java 教授的数据结构类的标准测试文件,我们几乎可以立即对其进行排序和 Huffman 编码,但这需要相当长的时间来编码。怎么回事?

【问题讨论】:

很可能,java 正在为您缓冲输入和输出。你在这里一次读/写一个字节。 @Blorgbeard iostream 也可以进行缓冲。 @j_kubik 我的立场是正确的。 你的编译标志是什么?在这种情况下,未优化的代码会非常慢,但优化有可能将其加速几个数量级。 @j_kubik g++ HammingEncode.cpp -O2 -o HammingEncode.out 【参考方案1】:

std::cin 在文本模式下打开,因此它会不断地寻找各种需要注意的东西(如换行符等)。

鉴于std::cin 输入流对字符的持续嗅探,我并不感到惊讶它需要更长的时间,但它似乎有点过分。以下,绕过iostream 并直接使用FILE 流可能会达到您的预期:

#include <cstdlib>
#include <cstdio>

int main(int argc, char *argv[])

    static unsigned char const codebook[] =
    
        0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78,
        0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF
    ;

    for (int c = std::fgetc(stdin); c!=EOF; c=std::fgetc(stdin))
    
        std::fputc(codebook[c >> 4], stdout);
        std::fputc(codebook[c & 0x0F], stdout);
    

    return EXIT_SUCCESS;

我在一个 10MB 的随机文件上测试了上面的 exact 代码,该文件加载了从az 的字符,当使用std::cinstd::cout 时,结果非常长。直接使用FILE 流,差异是巨大的。此答案中的所有代码均已使用Apple LLVM version 5.1 (clang-503.0.38) (based on LLVM 3.4svn) 使用-O3 优化进行了测试。

使用FILE

time ./hamming < bigfile.txt > bigfile.ham 
real 0m1.855s
user 0m1.812s
sys  0m0.041s

使用std::cinstd::cout

time ./hamming < bigfile.txt > bigfile.ham
real 0m23.819s
user 0m7.416s
sys  0m16.377s

std::cinstd::coutstd::cout.sync_with_stdio(false); 一起使用

time ./hamming < bigfile.txt > bigfile.ham
real 0m24.867s
user 0m7.705s
sys  0m17.118s

总之,哎哟。值得注意的是在系统中花费的时间。如果我有机会使用std::istream::get()put() 方法来更新它,我会的,但老实说,我不希望有任何奇迹发生。除非有一些魔法(对我来说,而不是对其他人)从std::cin 关闭 io xlat 的方法,否则FILE 流可能是一个合理的选择。我还没有调查啜饮std::cinrdbuf() 是否是一个可行的选择,但它可能也有希望。


编辑:使用std::istreambuf_iterator&lt;char&gt;

使用 streambuf 迭代器类有显着改进,因为它基本上绕过了所有内联 slat 垃圾,但它仍然不如 FILE 流高效:

#include <iostream>
#include <cstdlib>
#include <cstdio>

int main(int argc, char *argv[])

    static unsigned char const codebook[] =
    
        0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78,
        0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF
    ;

    std::istreambuf_iterator<char> cin_it(std::cin), cin_eof;
    std::for_each(cin_it, cin_eof, [](char c)
        
          std::cout.put(static_cast<char>(codebook[static_cast<unsigned char>(c) >> 4]));
          std::cout.put(static_cast<char>(codebook[static_cast<unsigned char>(c) & 0x0F]));
        );

    return EXIT_SUCCESS;

结果:

time ./hamming < bigfile.txt > bigfile.ham

real 0m6.062s
user 0m5.795s
sys  0m0.053s

请注意,system 现在与FILE 流结果相当,但是user 中其余 iostream 模板的开销似乎是一个痛点(但仍然比其他 iostream 尝试更好)。你赢了一些,你失去了一些=P


编辑:无缓冲系统 IO

为了做到完全公平,绕过所有运行时缓冲并仅依靠系统调用来完成这种疯狂的行为,以下内容也值得注意:

#include <cstdlib>
#include <cstdio>
#include <unistd.h>

int main(int argc, char *argv[])

    static unsigned char const codebook[] =
    
        0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78,
        0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF
    ;

    unsigned char c;
    while (read(STDIN_FILENO, &c, 1)> 0)
    
        unsigned char duo[2] =
        
            codebook[ c >> 4 ],
            codebook[ c & 0x0F ]
        ;
        write(STDOUT_FILENO, duo, sizeof(duo));
    

    return EXIT_SUCCESS;

如你所料,结果很糟糕:

time ./hamming < bigfile.txt > bigfile.ham

real 0m26.509s
user 0m2.370s
sys  0m24.087s

【讨论】:

static_cast&lt;unsigned char&gt;(c) 是多余的; fgetc 函数被定义为返回转换为unsigned char 范围的字符的值。 @MattMcNabb 内部的(索引码本)?是的,当cchar 而不是int 时,它是std::cin.get(c)char 代码的保留。可能应该把它去掉。感谢收看。 你能像我在回答中建议的那样测试自制缓冲吗? 很棒的回复,非常感谢!我认为读/写头有很多硬件开销,但是作为仍在学习 c++ 的人,这很棒:D【参考方案2】:

通过进行 2 个小改动,我的改进接近了一个数量级。

    添加 std::ios_base::synch_with_stdio(false)(没有明显差异,尽管影响通常是特定于实现的) 在写入之前缓冲输出(这是最大的不同)

更新后的代码如下所示:

int main()


    //encodes a file using the (8,4) Hamming Code.
    //usage : HammingEncode.out < inputFile > outputFile 
        unsigned char const codebook[] =  0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78, 0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF ;
        unsigned char in, nextByte;
        unsigned char const leftMask = 0xF0, rightMask = 0x0F;
        std::stringstream os;

        std::ios_base::sync_with_stdio(false);
        in = std::cin.get();
        while (std::cin) 
            nextByte = (in & leftMask) >> 4;
            os.put(codebook[nextByte]);
            nextByte = in & rightMask;
            os.put(codebook[nextByte]);
            in = std::cin.get();
        
        std::cout << os.rdbuf();

更新

我尝试了另一种实现方式——使用底层的std::streambuf

在我的系统上,原始代码大约需要 14 秒 来处理完整的国王詹姆斯圣经 - 大约 4.3 MB

我最初尝试中的代码大约需要 2.1 秒来处理。

这个新的实现需要 0.71 秒来处理同一个文档。

int main()


    //encodes a file using the (8,4) Hamming Code.
    //usage : HammingEncode.out < inputFile > outputFile 
    unsigned char const codebook[] =  0x00, 0x1E, 0x2D, 0x33, 0x4B, 0x55, 0x66, 0x78, 0x87, 0x99, 0xAA, 0xB4, 0xCC, 0xD2, 0xE1, 0xFF ;
    unsigned char in, nextByte;
    unsigned char const leftMask = 0xF0, rightMask = 0x0F;
    std::stringstream os;

    std::ios_base::sync_with_stdio(false);

std::streambuf * pbuf = std::cin.rdbuf();
    do 
        in = pbuf->sgetc();
        nextByte = (in & leftMask) >> 4;
        os << codebook[nextByte];
        nextByte = in & rightMask;
        os << codebook[nextByte];
     while (pbuf->snextc() != EOF);
    std::cout << os.rdbuf();

【讨论】:

+1 streambuf 读取端是有意义的,因为它本质上是使用istreambuf_iterator&lt;&gt; 会做的事情,减少开销。考虑到您的文件测试大小与我测试的 10MB 相比大约小 2.3 倍,与 10MB 文件的差异几乎符合预期。写得不错。【参考方案3】:

C++ iostreams 对效率低下有不好的看法,尽管不同的数字表明它是实现质量问题,而不是 iostream 的劣势。

无论如何,为了确保它不是慢速硬盘,您可以将执行时间与例如进行比较

cat file1 > file2

当然cat 会更快一些,因为它不会使您的数据大小翻倍。

然后尝试将您的代码的效率与以下内容进行比较:

#include <stdio.h>
#include <unistd.h>

int main()

    unsigned char buffer[1024*1024]; // 1MB buffer should be enough

    while (!eof(STDIN_FILENO))
        size_t len = read(STDIN_FILENO, &buffer[0], 1024*1024);
        write(STDOUT_FILENO, &buffer[0], len);
        write(STDOUT_FILENO, &buffer[0], len);
    

    return 0;

编辑:

对不起,我的错。试试

#include <stdio.h>
#include <unistd.h>

int main()

    unsigned char buffer[1024*1024]; // 1MB buffer should be enough

    size_t len = read(STDIN_FILENO, &buffer[0], 1024*1024);

    while(len > 0)
        write(STDOUT_FILENO, &buffer[0], len);
        write(STDOUT_FILENO, &buffer[0], len);
        len = read(STDIN_FILENO, &buffer[0], 1024*1024);
    

    return 0;

【讨论】:

这不编译,它不识别eof。我用 feof 替换它并编译,但给了我一个段错误。【参考方案4】:

如果我理解正确,对于您读取的每个字节,您将写入 2 个字节。所以输出文件将是输入的两倍大小。如果您的输入足够大 - 总 IO(读取 + 2 * 写入)时间将很重要。

在 Huffman 编码中情况并非如此 - 因为您通常写的比读的少,总 IO 时间会少得多。

编辑: 正如Blorgbeard 所说,这可能是缓冲的不同。 C++ 也进行缓冲,但可能默认缓冲区比 Java 小得多。此外,HDD 磁头应该在一个位置读取文件然后在另一个位置写入文件之间不断跳转这一事实 - 显着影响整体 IO 性能。

在任何情况下,编码都应该分块进行,以确保大块的顺序读写

【讨论】:

嗯,是的。但我不明白为什么写一个文件比直接用 Java 写要花两倍的时间。就像我用 Java 写两次这个文件一样,它会快得多。所以我想知道为什么这段代码需要 30 秒才能写入 8 MB 30 秒 8MB 听起来确实很慢。我会尝试实验:尝试逐字节读取文件,并且对于每个字节,只需将读取的字节两次写入输出文件。这将有助于了解问题是在 IO 中还是在算法本身中(不太可能)。但重要的是读取字节然后立即写入两次(而不是读入缓冲区然后写入两次缓冲区) @DenisItskovitch 我不确定我是否知道如何在不使用缓冲的 iostream 的情况下直接编写。我不能做 nextByte = std::cin.get() & rightMask; std::cout @JonCohen 我不是直接的意思。我的意思是完全按照您在问题中所做的那样读/写,但没有编码 试过了,时间一样长:(

以上是关于我为类编写了这个汉明编码代码。为啥这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Tkinter 窗口打开这么慢?

为啥我的代码这么慢?

为啥.NET BASE64编码效率这么低?

为啥这段代码运行得这么慢?

为啥这个交叉连接在 Linq 中这么慢?

为啥这个 select 语句这么慢?