优化I/O

优化I/O

写在开头

  • 读写文件是再普通不过的活动了,以至于开发人员往往忽略了它们,但是实际上它们确实非常耗时的程序活动

  • 互联网世界受限于数据传输速率和繁忙的服务器,响应延迟可能是以毫秒而非秒计量的。当数据传向远方的计算机时,即使是光速传输,传输时间也会成为一个影响性能的因素。

  • I/O的另外一个问题是在用户的程序与旋转的磁盘或是网卡之间有太多的代码。为了使I/O尽可能高效,必须尽量减少所有这些代码的性能开销。

读取文件的秘诀

std::string stream_read_streambuf_stringstream_noarg(std::istream& f) {
    std::stringstream s;
    std::copy(std::istreambuf_iterator(f.rdbuf()), std::istreambuf_iterator(), std::ostreambuf_iterator(s));     return s.str();  // 除非编译器和标准库实现都支持移动语义,否则这样做会导致内存分配和复制
} 
  • 创建一个吝啬的函数签名

    void stream_read_streambuf_stringstream(std::istream& f, std::string& result) {
        std::stringstream s;
        std::copy(std::istreambuf_iterator(f.rdbuf()), std::istreambuf_iterator(), std::ostreambuf_iterator(s) );
        std::swap(result, s.str()); // std::swap()对许多标准库类的特化实现都是调用它们的swap()成员函数,其会交换指针,这远比内存分配和复制开销小
    }
    // 另一种复制流迭代器的文件读取函数
    void stream_read_streambuf_string(std::istream& f, std::string& result) {
        result.assign(std::istreambuf_iterator(f.rdbuf()), std::istreambuf_iterator());
    }
    
  • 缩短调用链

    // 添加stream到stringstream中,一次一个字符
    void stream_read_streambuf(std::istream& f, std::string& result) {
        std::stringstream s;
        s << f.rdbuf(); // <<运算符可能会绕过istream API直接调用流缓冲区
        std::swap(result, s.str());
    }
    
  • 减少重新分配

    // 通过将流指针移动到流尾部,读取偏移量后再将流指针复位到流头部来计算流长度
    void stream_read_streambuf_string_reserve(std::istream& f, std::string& result) {
        f.seekg(0,std::istream::end);
        std::streamoff len = f.tellg();
        f.seekg(0);
        if (len > 0)
            result.reserve(static_cast(len)); 
        result.assign(std::istreambuf_iterator(f.rdbuf()),  std::istreambuf_iterator());
    }
    
    // 计算流长度
    std::streamoff stream_size(std::istream& f) {
    	std::istream::pos_type current_pos = f.tellg();
    	if (-1 == current_pos)
    		return -1;
    	f.seekg(0,std::istream::end);
    	std::istream::pos_type end_pos = f.tellg();
        f.seekg(current_pos);
    	return end_pos - current_pos;
    }
    
  • 更大的吞吐量——使用更大的输入缓冲区
    C++流包含一个继承自std::streambuf的类,用于改善从操作系统底层以更大块的数据单位读取文件时的性能。

    pubsetbuf()必须在打开流后和从流中读取任意字符之前被调用。如果流中有一个状态位被设置了,那么函数调用就会失败。在流关闭之前缓冲区必须一直保持有效。
    性能提升程度不大,当缓冲区的大小超过一定程序后,性能就几乎没有提升了。但它还是一个很重要的因素。

    std::ifstream in8k;
    in8k.open(filename);
    char buf[8192];	// set fat static buffer
    in8k.rdbuf()->pubsetbuf(buf, sizeof(buf));
    

    pubsetbuf()必须在打开流后和从流中读取任意字符之前被调用。如果流中有一个状态位被设置了,那么函数调用就会失败。在流关闭之前缓冲区必须一直保持有效。
    性能提升程度不大,当缓冲区的大小超过一定程序后,性能就几乎没有提升了。但它还是一个很重要的因素。

  • 更大的吞吐量——一次读取一行

    void stream_read_getline(std::istream& f, std::string& result) {
        std::string line;
        result.clear();
        while (getline(f, line))
            (result += line) += "\n";
    }
    void stream_read_getline_2(std::ifstream& f, std::string& result, std::streamoff len = 0) {
        std::string line;
        result.clear();
        if (len > 0)
            result.reserve(static_cast(len));
        while (getline(f, line))		
            (result += line) += "\n";
    }
    bool stream_read_sgetn(std::istream& f, std::string& result) {
        std::streamoff len = stream_size(f);
        if (len == -1)
            return false;
        result.resize (static_cast(len));    
    	f.rdbuf()->sgetn(&result[0], len);	
    	return true;
    }
    
  • 再次缩短函数调用链

    bool stream_read_string(std::istream& f, std::string& result) {
        std::streamoff len = stream_size(f);
        if (len == -1)
            return false;
        result.resize (static_cast(len));    
    	f.read(&result[0], result.length());	// read()能够将字符直接复制到缓冲区
    	return true;
    }
    // 这个函数对那些实现方法违反了连续存储字符的标准的新奇的字符串也适用
    bool stream_read_array(std::istream& f, std::string& result) {
        std::streamoff len = stream_size(f);
        if (len == -1)
            return false;
        std::unique_ptr<char> data(new char[static_cast<size_t>(len)]);
        f.read(data.get(), static_cast<std::streamsize>(len));
    	result.assign(data.get(), static_cast<std::string::size_type>(len));
    	return true;
    }
    
  • 无用的技巧

    有些人建议自己编写streambuf来改善性能,其问题在于它试图去优化一种低效算法。任何性能改善效果可能都是源于在自定义的streambuf中使用了8KB缓冲区,而这其实只用几行代码就能够实现。

    // from: http://stackoverflow.com/questions/8736862
    // This was a dreadful failure; very slow
    class custombuf : public std::streambuf
    {
    public:
        custombuf(std::string& target): target_(target) {
            this->setp(this->buffer_, this->buffer_ + bufsize - 1);
        }
    private:
        std::string& target_;
        enum { bufsize = 8192 };
        char buffer_[bufsize];
        int overflow(int c) {
            if (!traits_type::eq_int_type(c, traits_type::eof()))
            {
                *this->pptr() = traits_type::to_char_type(c);
                this->pbump(1);
            }
            this->target_.append(this->pbase(), this->pptr() - this->pbase());
            this->setp(this->buffer_, this->buffer_ + bufsize - 1);
            return traits_type::not_eof(c);
        }
        int sync() { this->overflow(traits_type::eof()); return 0; }
    };
    std::string stream_read_custombuf(std::istream& f) {
            std::string data;
            custombuf sbuf(data);
            std::ostream(&sbuf) << f.rdbuf() << std::flush;
            return data;
    }
    

    有些人建议自己编写streambuf来改善性能,其问题在于它试图去优化一种低效算法。任何性能改善效果可能都是源于在自定义的streambuf中使用了8KB缓冲区,而这其实只用几行代码就能够实现。

写文件

void stream_write_line(std::ostream& f, std::string const& line) {
        f << line << std::endl; // std::endl会刷新输出
}
void stream_write_line_noflush(std::ostream& f, std::string const& line) {
        f << line << "\n"; // std::ofstream只是将几个大数据块传递给了操作系统
}

将整个文件内容先保存在一个字符串中然后输出的性能更快。

从std::cin读取和向std::cout中写入

  • 当从标准输入中读取数据时,std::cin是与std::out紧密联系在一起的。要求从std::cin中输入会首先刷新std::cout,这样交互控制台程序就会显示出它们的提示。调用istream::tie()可以得到一个指向捆绑流的指针,前提是该捆绑流存在。调用istream::tie(nullptr)会打破已经存在的捆绑关系。刷新操作的开销非常昂贵。

  • C++流在概念上是与C的FILE*对象的stdin和stdout连接在一起的。std::cout与stdout的连接是实现定义的。多数标准库实现默认都会直接将std::cout发送至stdout。stdout默认是按行缓存的,在C++的输入输出流中没有这种方式。每当stdout遇到新的一行,它都会刷新缓冲区。

  • 切断连接有助于改善性能。调用静态成员函数std::ios_base::sync_with_stdio(false)可以打破这种连接,但代价是如果程序同时使用了C和C++的IO函数,那么交叉行为将变得不可预测。

小结

  • 不论你是在哪个网站上看到的,互联网上的“快速”文件I/O代码不一定快。

  • 增大rdbuf的大小可以让读取文件的性能提高几个百分点。

  • 我测试到的最快的读取文件的方法是预先为字符串分配与文件大小相同的缓冲区,然后调用std::streambuf::sgetn()函数填充字符串缓冲区。

  • std::endl会刷新输出。如果你并不打算在控制台上输出,那么它的开销是昂贵的。

  • std::cout是与std::in和stdout捆绑在一起的。打破这种连接能够改善性能。

posted @ 2021-03-13 10:44  睿阳  阅读(129)  评论(0编辑  收藏  举报