File I/O 效率 C vs C++ (一)

继续关注文件读写...这次测试写的效率

其实关于这个问题的讨论似乎总会以类似语言信仰问题而告终,再加 C++ IO 库的复杂性,很多半调子 C++ 程序员总会出现各种误用,反过来却作为攻击 C++ IO 效率低的凭证。
比如经常有人边在输出时大量使用类似
    fout<<...<<endl;
的语句边嚷嚷着写文件速度超慢的,只能说这些人根本不知道自己写的句子都做了什么...
 
所以说在评价任何东西之前先要做到最起码的了解
咱也算是大致研究过 C++ IO stream 各方面的内容,虽然不能说完全掌握仍在学习中,但自认为还是可以写点东西的。
 
总之还是用数据说话。
平台 XPsp3 + VC2008 Express Edition SP1 + STLport / MinGW(GCC4.4.0)
 
实验内容:
共做了三种类型写入的比较,每种类型采用几种不同方法实现:
1. 纯字节流写入
     (1) C fputs()
     (2) C fprintf()
     (3) C++ ofstream<<
     (4) C++ ofstream.rdbuf()->sputn()
2. 宽字符流通过转码写入
     (1) C++ locale + wofstream<<
     (2) C++ codecvt<> facet + ofstream.rdbuf()->sputn()
     (3) WinAPI WideCharToMultiByte() + ofstream<<
3. 格式化写入 (一个 int 一个 double + 一个字符串)
     (1) C fprintf()
     (2) C++ ofstream<<
     (3) C++ ostream.rdbuf()->sputn() sputc() + num_put<> facet
 
前两类比较时写入的内容是完全一致的,也可以横向做下参照。
 
结果:(由于和硬盘寻道时间等也有关系,尽量多次测试取受影响最小的值)
以 C Runtime library 的 fputs 函数所用时间为 1.0 做基准

VC2008EE + VC STL:
text out C fputs()                      1.0
text out C fprintf()                    2.0
text out C++ ofstream <<                1.2
text out C++ ofstream rdbuf()->sputn()  0.8
wText out C++ wofstream deflocale      25.0
wText out C++ locale codecvt            7.5
wText out C++ ofstream + WinAPI         1.5
Format data out C fprintf()             2.0
Format data out C++ ofstream  <<        4.0
Format data out C++ ofs num_put facet   3.0
 
首先用 VC 自带的标准库,可以看到直接用 C++ fstream 的 << 效率与 C 库函数相比还是略低一些,但差距并没有到不可忍受的地步,考虑到 C++ IO stream 的各种特性,这些还是可以接受的。
而直接调用下层 rdbuf()->sputn() 函数却比 fputs() 效率更高,让我对 C++ IO 库的进一步优化仍有希望。
在 Unicode 大行其道的今天,宽字符的读写已成为必需,但使用 locale 配合 wstream 来做的话效率确实无法接受,从上面看居然比调用 WinAPI 的方法慢几十倍。我还是严重怀疑 VC STL 的实现有问题,只是换做直接调用 codecvt<> facet 就能提高好几倍效率。
格式化读写一向是 C++ IO stream 被重点诟病的地方,与 fprintf() 函数整相差一倍,即使直接调用 num_put (去掉了构造 ios_base::sentry 对象产生的流同步、空白跳过等操作),仍然有 50% 的差距。
其实从理论上来说,C++ 的格式化读写应当是比 C 的 fprintf() 函数要快的,因为 fprintf() 总要有一个解析格式字符串的过程,这个只能放在运行时,而 C++ 的格式是通过多个连续函数调用控制的,可以在编译时即进行优化。但实践往往存在各种变数 = =
 
VC2008EE + STLport5.2.1:
text out C fputs()                      1.0
text out C fprintf()                    2.0
text out C++ ofstream <<                0.7
text out C++ ofstream rdbuf()->sputn()  0.6
wText out C++ wofstream deflocale       7.5
wText out C++ locale codecvt            7.0
wText out C++ ofstream + WinAPI         1.0
Format data out C fprintf()             2.0
Format data out C++ ofstream  <<        2.0
Format data out C++ ofs num_put facet   1.7
 
STLport 的 C 库函数完全是照搬 VC 的标准运行库,而只是重新实现了整个 C++ 库,所以 C 函数的效率与上一例是相同的,可直接横向比较。
面对这样的结果,我只能说 STLport 太赞了!!再考虑到其他的种种特性,赶紧扔掉 VC STL 全部换用 STLport 吧
不过转码看来确实是一个平台相关的特性,仍然无法比过 WinAPI 的效率

MinGW GCC4.4.0 + libstdc++:
text out C fputs()                      1.5
text out C fprintf()                    2.7
text out C++ ofstream <<                6.0
text out C++ ofstream rdbuf()->sputn()  2.7
mingw libstdc++ doesnot surpport other locale...
mingw libstdc++ doesnot surpport other locale...
wText out C++ ofstream + WinAPI         2.1
Format data out C fprintf()             3.0
Format data out C++ ofstream  <<        6.6
Format data out C++ ofs num_put facet   5.8
 
MinGW 貌似比较慢,是因为 MinGW 默认输出按 UTF-8 编码,对中文来说,字节数是 ANSI 的 1.5 倍。只有调用 API 的那个例子保持了 ANSI 编码。
除以 1.5 的话可以看到 C 库函数的效率与 VC 差不多,但 C++ 的效率比 VC 略低,与 STLport 比就差更远了。毕竟 GCC + libstdc++ 在 Win 平台不是原生支持,只作为跨平台的特性这样也足够了。
没有用 MinGW + STLport 做实验,不知能不能达到 VC 的效率。
 
-------------------------------------------------------------
以前使用 C++ 的 IO stream 做输入输出时总担心效率问题,现在有了 STLport 做支持就可以放心大胆的用了。
但可以看到 C++ 的流式 IO 非常依赖于库实现,在各平台上的表现大概不如 C 库函数来得稳定。
而且使用除 "C" 之外的 locale 时效率确实还是有问题,转码的话还是直接调用平台 API 省时省力又高效。MinGW 考虑不引入 locale 部分也是有道理的啊...
 
这次全是 O,下次再比较 I 的情况...
 
最后按惯例是程序代码:
  1. // test the performance of C I/O & C++ streams & C++ codecvt facet
  2. #include<cstdio>
  3. #include<iostream>
  4. #include<fstream>
  5. #include<locale>
  6. #include<cstdlib>
  7. #include<ctime>
  8. #include<windows.h>
  9. using namespace std;
  10.  
  11. const int testsize = 50000;
  12.  
  13. int main(){
  14.     char cstr[] = "这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串";
  15.     wchar_t wstr[] = L"这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串这是实验字符串";
  16.     char buffer[200];
  17.     int cstrlen = strlen(cstr);
  18.     locale defloc("");
  19.  
  20.     clock_t start;
  21.     FILE *cfile;
  22.     ofstream fout;
  23.     wofstream wfout;
  24.  
  25.     // pure text ...........................
  26.     start = clock();
  27.     cfile = fopen("text_out_C_fputs.txt", "w");
  28.     for(int i = 0; i < testsize; ++i){
  29.         fputs(cstr, cfile);
  30.         fputc('\n', cfile);
  31.     }
  32.     fclose(cfile);
  33.     printf("Text out C fputs: %d\n", clock()-start);
  34.  
  35.     start = clock();
  36.     cfile = fopen("test_out_C_fprintf.txt", "w");
  37.     for(int i = 0; i < testsize; ++i){
  38.         fprintf(cfile, "%s\n", cstr);
  39.     }
  40.     fclose(cfile);
  41.     printf("Text out C fprintf: %d\n", clock()-start);
  42.  
  43.     fout.clear();
  44.     start = clock();
  45.     fout.open("text_out_Cpp_ofstream.txt");
  46.     for(int i = 0; i < testsize; ++i){
  47.         fout << cstr << '\n';
  48.     }
  49.     fout.close();
  50.     printf("Text out Cpp ofstream: %d\n", clock()-start);
  51.  
  52.     fout.clear();
  53.     start = clock();
  54.     fout.open("text_out_Cpp_rdbuf.txt");
  55.     for(int i = 0; i < testsize; ++i){
  56.         fout.rdbuf()->sputn(cstr, cstrlen);
  57.         fout.rdbuf()->sputc('\n');
  58.     }
  59.     fout.close();
  60.     printf("Text out Cpp rdbuf: %d\n", clock()-start);
  61.  
  62.     // wchar_t text ...............................
  63.     wfout.clear();
  64.     start = clock();
  65.     wfout.open("wtext_out_Cpp_wofs_defloc.txt");
  66.     wfout.imbue(defloc);
  67.     for(int i = 0; i < testsize; ++i){
  68.         wfout << wstr << L'\n';
  69.     }
  70.     wfout.close();
  71.     printf("wText out Cpp wofs with default locale: %d\n", clock()-start);
  72.  
  73.     fout.clear();
  74.     start = clock();
  75.     char *next;
  76.     const wchar_t *wnext;
  77.     mbstate_t st;
  78.     fout.open("wtext_out_Cpp_codecvt_facet.txt");
  79.     for(int i = 0; i < testsize; ++i){
  80.         use_facet<codecvt<wchar_t, char, mbstate_t> >(defloc).out(
  81.             st, wstr, wstr+sizeof(wstr)/2-1, wnext,
  82.             buffer, buffer+sizeof(buffer)-1, next);
  83.         fout.rdbuf()->sputn(buffer, next-buffer);
  84.         fout.rdbuf()->sputc('\n');
  85.     }
  86.     fout.close();
  87.     printf("wText out Cpp ofs with codecvt facet: %d\n", clock()-start);
  88.  
  89.     fout.clear();
  90.     start = clock();
  91.     fout.open("wtext_out_Cpp_ofs_WinAPI.txt");
  92.     for(int i = 0; i < testsize; ++i){
  93.         WideCharToMultiByte(CP_ACP, 0, wstr, -1, buffer, 200, NULL, NULL);
  94.         fout << buffer << '\n';
  95.     }
  96.     fout.close();
  97.     printf("wText out Cpp ofs with WideCharToMultiByte API: %d\n",
  98.         clock()-start);
  99.  
  100.     // Format out ...........................................
  101.     srand((unsigned)time(NULL));
  102.  
  103.     char datastr[] = "TestDataString实验格式化字符串";
  104.     start = clock();
  105.     cfile = fopen("format_data_out_C_fprintf.txt", "w");
  106.     for(int i = 0; i < testsize; ++i){
  107.         fprintf(cfile, "%d %lf %s\n",
  108.             rand(), double(rand())/RAND_MAX, datastr);
  109.     }
  110.     fclose(cfile);
  111.     printf("Format data out C fprintf: %d\n", clock()-start);
  112.  
  113.     fout.clear();
  114.     start = clock();
  115.     fout.open("format_data_out_Cpp_ofstream.txt");
  116.     for(int i = 0; i < testsize; ++i){
  117.         fout << rand() << ' ' << double(rand())/RAND_MAX << ' '
  118.             << datastr << '\n';
  119.     }
  120.     fout.close();
  121.     printf("Format data out Cpp ofstream: %d\n", clock()-start);
  122.  
  123.     fout.clear();
  124.     start = clock();
  125.     fout.open("format_data_out_Cpp_ofs_facet.txt");
  126.     for(int i = 0; i < testsize; ++i){
  127.         use_facet<num_put<char> >(locale::classic()).put(
  128.             ostreambuf_iterator<char>(fout), fout, ' ', (long)rand());
  129.         fout.rdbuf()->sputc(' ');
  130.         use_facet<num_put<char> >(locale::classic()).put(
  131.             ostreambuf_iterator<char>(fout), fout, ' ',
  132.             double(rand())/RAND_MAX);
  133.         fout.rdbuf()->sputc(' ');
  134.         fout.rdbuf()->sputn(datastr, sizeof(datastr) - 1);
  135.         fout.rdbuf()->sputc('\n');
  136.     }
  137.     fout.close();
  138.     printf("Format data out Cpp ofs with facet: %d\n", clock()-start);
  139.  
  140.     system("pause");
  141. }

C/C++ Comments(0) 2009年10月31日 16:22

fstream 文件 IO 点滴

很多时候较大数据量的文件 IO 总是成为瓶颈,为了提高效率,有时想要先将文件大块大块的读入再行处理。下面分析两种惯常的处理手法。

1. 将文件一次性读入 string 中。

貌似 std::getline 、 istream::getline 或是 operator<< operator>> 等都不提供一次读到文件结尾的机制,只有 istreambuf_iterator 可以做到:

ifstream in("input.txt");
string instr((istreambuf_iterator<char>(in)), istreambuf_iterator<char>());

string 的构造函数前一个参数要多加一层 () 以免编译器误认为是函数声明 = = ...

这样读入 string 会随着内容动态增长,空间不足时会触发额外的 realloc 及 copy 操作,为提高效率有必要预分配足够的空间:

ifstream in("input.txt");
in.seekg(0, ios::end);
streampos len = in.tellg();
in.seekg(0, ios::beg);

string instr;
instr.reserve(len);
instr.assign(istreambuf_iterator<char>(in), istreambuf_iterator<char>());

2. 将文件一次性读入 stringstream 中。

filebuf 和 stringbuf 无法直接通过 rdbuf() 重定向,因此从 filebuf 到 stringbuf 需要一次 copy 操作。最简单的方法是直接复制整个 streambuf :

ifstream in("input.txt");
stringstream ss;
ss<<in.rdbuf();

与 string 的情况相同,这里同样也有一个空间 realloc 及 copy 的问题。但 streambuf 的缓冲区不是那么方便操作的,解决方法是我们给他手动指定一个空间:

ifstream in("input.txt");
in.seekg(0, ios::end);
streampos len = in.tellg();
in.seekg(0, ios::beg);

vector<char> buffer(len);
in.read(&buffer[0], len);

stringstream ss;
ss.rdbuf()->pubsetbuf(&buffer[0], len);

最后再顺便 BS 一下 VC 的 STL = =...

虽然 VC 的编译器效率没的说,但被 STL 拖后腿的话不就白搭了嘛。在文件 IO 方面 (fstream) 比起 MinGW (GCC 4.4.0) 带的要慢好几倍。GCC 的 fstream 格式化读写效率与 C 的比已经不分伯仲,以后应该还会有进一步的提升空间 (编译时格式控制 vs 执行时)

另外上面最后一段程序在 VS2008 (VC9.0) 下应该无法得到预想的结果,跟踪进去看了一下,VC 标准库里的 pubsetbuf 函数体居然是空的!内容如下(中间还有一层函数调用):

virtual _Myt *__CLR_OR_THIS_CALL setbuf(_Elem *, streamsize)
        {       // offer buffer to external agent (do nothing)
        return (this);
        }

看来是等着我们来继承了啊 = = 。而在 MinGW (GCC 4.4.0) 中可以得到预期的结果。

C/C++ Comments(0) 2009年10月06日 18:24

从 std::list 中 size() 的时间复杂度引出的讨论...

很奇怪的,或者说是一个不应成为问题的问题...
std::list 的 size() 方法时间复杂度是多少?第一感觉应该是 O(1) 没错吧,多一个变量用于储存链表长度应该是很轻易的事情。于是有了下面这段代码:

#include<iostream>
#include<list>
#include<ctime>
using namespace std;

int main(){
    time_t start, finish;
    int num = 0;
    list<int> coll;

    start = clock();
    for(int i=0;i<10000;++i){
        coll.push_back(i);
        num += coll.size();
    }
    finish = clock();
    cout<<finish - start<<"   num:"<<num<<endl;

    coll.clear();
    start = clock();
    for(int i=0;i<10000;++i){
        coll.push_back(i);
    }
    finish = clock();
    cout<<finish - start<<endl;
    return 0;
}

对两个循环分别计时比较。前一个循环只比后一个多了一句 num += coll.size(); 为了使编译器确实生成 list::size() 的代码。
在 MinGW 5.1.4 中 (GCC 3.4.5) 编译结果运行如下:

450   num:50005000
10

可以看到,前一个循环居然比后一个多花了几乎 45 倍的时间...当我把循环次数从 10000 加到 100000 时程序半天没出结果...

由此有理由猜测 std::list 的 size() 方法难道是 O(N) 的?果然,在头文件中发现了这一段:

size_type
size() const
{ return std::distance(begin(), end()); }

 

直接调用 <algorithm> 算法库函数 distance() 计算元素个数……怪不得这么慢。然后又用 VS2008 (VC9.0)编译,结果如下:

30   num:50005000
60

奇怪的是前一个循环居然比后一个还快...不过至少知道 VS2008 (VC9.0)里的 size() 应该是 O(1) 的。同样查看了一下代码,如下:

size_type size() const
    {   // return length of sequence
    return (_Mysize);
    }

_Mysize 是一个 size_type 类型的变量。疑问解决。不过又有了新问题:

--------------- 咱 -- 是 -- 分 -- 隔 -- 线 ------------------

为什么 GCC 里要把 list::size() 的复杂度搞成 O(N)?

一通搜索后终于看到有这样的讨论:关于 list::splice() 函数。

list 是链表结构,它的优势就在于可以 O(1) 的时间复杂度任意插入删除甚至拼接 list 片段(删除时可能不是,因为要释放内存),list::splice() 是一个很强大的功能,它可在任意位置拼接两个 list,这正是 list 的优势。如果我们在类内部以一个变量储存 list 的长度,那么 splice() 之后新 list 的长度该如何确定?这是一个很严峻的问题,如果要在拼接操作时计算拼接部分的长度,那么将把 O(1) 的时间变成 O(N),这么一来 list 相对 vector 的优势就消失殆尽。

面对这个问题,GCC 和 VC 的 STL 库作者们做了不同的选择。GCC 选择舍弃在 list 内部保存元素数量,而在 size() 时直接从头数到尾,这便出现了开头看到的 O(N) 时间才算出 size();相反,VC 中有了变量 _Mysize ,无论在 insert() erase() splice() 或是 push() pop() 时都需要对其做相应修改。在上面的两个试验中已经看出同样是 10000 个 push_back() 操作,VC 花的时间比较长,不过也仅仅是一个 inc 指令,差别很小就是了。上面几种会改变 list 内容的操作中,大部分对元素数量的影响只是 +1 或 -1,只有 splice() 需要计算拼接部分元素个数,这个差别就大了,咱还是继续用实验证明吧:

#include<iostream>
#include<list>
#include<ctime>
using namespace std;

int main(){
    time_t start,finish;
    list<int> col;
    col.push_back(1);
    col.push_back(10000);

    list<int> col2;
    start = clock();
    for(int i=2;i<10000;++i)
        col2.push_back(i);
    finish = clock();
    cout<<finish - start<<endl;

    int num = 0;
    start = clock();
    for(int i=0;i<10000;++i){
        col.splice(++col.begin(),col2,++col2.begin(),--col2.end());
        num += *(++col.begin());
        col2.splice(++col2.begin(),col,++col.begin(),--col.end());
        num += *(++col2.begin());
    }
    finish = clock();
    cout<<finish - start<<"   num:"<<num<<endl;
    return 0;
}

首先是 MinGW (GCC 3.4.5) 的结果:

10
0   num:60000

可以看到 10000 次 push 是 10,相对的 20000 次 splice() 几乎没花时间 = =

然后是 VS2008 (VC9.0):

20
2714   num:60000

差别非常明显,花了2秒多才完成。当我把循环次数改成 100000 后 GCC 仍是眨眼间的事,VC 却长时间运行无结果……

怎么说呢,GCC 显然是追求效率至上,尽量体现出 list 的优势所在,不过我觉得这么一来倒不如干脆不提供 list 的 size() 方法,有需求的程序员可以自己维护一个变量记录长度,以免误认为 size() 是 O(1) 的而犯下严重错误。相对的 VC 强调功能性和整体效率,可能在实际中需要对链表一段内容做 splice() 操作的机会远远小于求 size() 的操作,所以舍弃前者而保留后者,不过要维护 _Mysize 其他相关函数中也增加了开销。一个见仁见智的问题,我觉得还是 GCC 的选择比较好,list 的优势应该保留,但能在 size() 函数处给个 warning 什么的就好了。

我想还有一个选择是这样:在 list 内部用一个 bool 变量指示当前内部 size 值是有效还是无效。在通常操作时 bool 保持 true,这样在 size() 时直接返回原值即可;在 splice() 后将此 bool 值置为 false 并不计算长度,直到最后又有需要 size() 时发现 bool 是 false 则从头再来一遍 distance() 并再将 bool 置为 true。暂时只想出这么一个算是折中的方法,基本上都能保持两边 O(1) 的效率,但相应其他各关于元素数量的函数内部都要多一个判断当前 size 值是有效还是无效并选择是否改变其值。反正总是不能非常完美

嘛...本来只是发现 size() 的效率问题,没想到却扯出这么一桩事出来...也算长知识了吧

C/C++ Comments(0) 2009年5月10日 05:49

STL locale 使用小结

locale 是多种 facet 的容器,每种 facet 管理与 locale 相关的一种功能。
facet 除了按名称区别外,更常用的是按 category 来分类。分类情况如下:
 
locale::ctype 类别,包括以下 facet 模板
ctype         // 字符分类和转换
codecvt     // 字符编码转换
locale::collate 类别,包括以下 facet 模板
collate         // 字符串校对
locale::message 类别,包括以下 facet 模板
messages // 从信息目录中获得本地化信息
locale::numeric 类别,包括以下 facet 模板
numpunct   // 有关数字和布尔运算表达式中标点符号及格式信息
num_get     // 代表数字或布尔值的字符串的解析
num_put     // 代表数字或布尔值的格式化字符串的生成
locale::monetary 类别,包括以下 facet 模板
moneypunct // 货币表达式中的标点符号及格式
money_get   // 代表货币值的字符串的解析
money_put   // 代表货币值的格式化字符串的生成
locale::time 类别,包括以下 facet 模板
time_get        // 代表日期和时间的字符串的解析
time_put        // 代表日期和时间的格式化字符串的生成
 
使用方法:
locale 对象是不可变的,即在它们的生命周期中,它们的内容不可改变。所包含的 facet 不能进行修改或替换,同时 facet 不能增加或删除。
鉴于以上特性,在使用 locale 时一般都是根据需要生成新的 locale 对象,然后选入IO流中。因此 locale 的构造函数就变得十分多样,方便我们以各种形式构造所需要的locale 对象。
例如,需要 std::wostream 输出中文,我们就需要 locale("chs") 中编码转换相关的功能,但若直接选择 locale("chs"),输出数字时也会进行转换处理,例如将 1234 输出为 "1,234"。为了避免这一转换,就需要保留原 locale("C") 中除了字符相关的其他facet。如下处理即可
locale loc("chs", locale::ctype);
此函数以 global 对应的 locale (一般是 locale("C") ) 初始化 loc 并选择 locale("chs") 的字符相关 facet ,这样我们就可以用 loc 正确输出中文,并保持输出数字时不进行其他处理
其他可参阅 MSDN 中关于 locale 的构造函数说明,解释很详细,用法很简单。
此外,locale 对象还可使用 combine 成员函数 选取其他 locale 中指定 facet 进行组合。总之接口多样,不过也一定程度上增加了对 locale 学习的复杂性。
 
最后,还有更简便的解决方案,即全局 locale 对象。
如果你建立了一个流对象,并且没有规定流使用的 locale 对象,那么流使用全局 locale 对象。如果我们在程序开始处改变全局 locale 对象为我们需要的形式,以后就不需再反复设定每个流的 locale 了。如
locale::global(locale("",locale::ctype));
除了将字符处理部分改为当前系统默认的编码方式外其他不变,这样一般就满足需求了。
再加一条,当流建立后再改变全局locale,则对已建立的流无影响。还有改变 C++ 的全局 locale 对象可能会影响 C 的全局 locale 设定,请不要混用。既然用 locale 对象,就全部采用 C++ 的风格吧

C/C++ Comments(0) 2009年4月25日 07:41

关于 VC 中的 STL string 实现

今天小实验一下发现 VC9 的 STL string 实现中没有使用 引用计数和写时拷贝
在 VC6.0 时默认还是有这两个特性的,从 VC7.0 开始貌似就取消了,大概是考虑到线程安全问题
如果我不用多线程,这两个特性应该还是可以一定程度上提高效率的吧
不然在返回一个 string 时是否也要考虑用 auto_ptr 了呢...
看来有必要自己实现一个 string ,或者偶尔考虑用一下其他库的...
 
不过从一致性考虑,感觉还是显式使用智能指针实现以上功能比较好,不然其他各种自定类型是不是也有必要封装一个 share_ptr 类似特性呢。

 

C/C++ Comments(0) 2009年4月25日 07:35