原文地址:
http://student.csdn.net/space.php?uid=112600&do=blog&id=14316
部门最近在搞JVM上的动态语言,比如Groovy。在享受了动态语言的种种灵活之后,性能自然而然被拿出来PK。
然后玩Python的同事就旧事重提,从网上找来一段Python代码,很多Python的人都知道了,很多C++的人也知道了,它跑得很快。
为了让文章好看一点,我来编一个故事,说,有个软件公司,正要招刚从大学毕业的C++程序员和Python程序,面试题是同一道:
有一个15万行的文档,其中很多行的内容相同的,请写一段代码,读入这文件,尽可能快地将不重复的行内容输出到新文件,注意次序不要改变”,比如,有5行内容:
B1
B2
A2
B2
B1
要求新文件内容为:
B1
B2
A2
小P新学习Pythont不久,他写出代码如下:
- import time
- if __name__ == "__main__" :
- beg = time.time()
- hashtable = {}
- fi = file("test.txt", "r")
- fo = file("test_dst.txt", "w")
- count = 0;
- for line in fi :
- if not hashtable.has_key(line) :
- hashtable[line] = 1
- fo.write(line)
- count = count +1
- fo.close()
- fi.close()
- end = time.time()
- print "count=", count, "time=", (end - beg)*1000, "ms"
这是一段中规中矩的Pythont程序,符合Python的风格:看不出是新手还是老手写的。:)
OK,在我的机器上,我准备了一个15万文字,但不重复行只有4万行的文件,上面的代码运行时间是281 毫秒。性能比一会儿的相同逻辑的C++代码还要快,所以小P被相中了。
接来是C++的面试。 嗯,不懂 hash的同学可能要被抛弃了,用vector,list的都会非常非常的慢,或许用map的人可以考虑,理由是严格地讲STL里是没有hash数据结构的。但反过来说,有在学校里认真学习数据结构,并且有实际使用STL的C++程序员,应该都知道几乎所有版本的STL实作,都有附加实现了hash——再者,还是从性能上,用map也还是很慢很慢,你应该知道在find上,它的复杂度和hash表是什么倍数。
还好,故事中的小C,他虽然学习C++也不久,但他懂hash,也写了一段中规中矩的代码:
- #include <iostream>
- #include <string>
- #include <fstream>
- #include <sstream>
- #include <ctime> //for clock()
- #include <ext/hash_map>
- using namespace std;
- using namespace __gnu_cxx;
- struct string_hash
- {
- size_t operator()(const string& str) const
- {
- return __stl_hash_string(str.c_str());
- }
- };
- struct string_compare
- {
- bool operator () (string const& str1, string const& str2)
- {
- return str1 == str2;
- }
- };
- int main()
- {
- clock_t beg = clock();
- hash_map<string, int, string_hash, string_compare> hastable;
- ifstream fi ("test.txt");
- ofstream fo ("test_dst.txt");
- string line;
- size_t count = 0;
- for (getline(fi, line); !fi.eof(); getline(fi, line))
- {
- if (hastable.find(line) == hastable.end())
- {
- hastable[line] = 1;
- fo << line << endl;
- ++count;
- }
- }
- fo.close();
- fi.close();
- clock_t end = clock();
- cout << "count = " << count << ", time = " << (end-beg) << "ms" << endl;
- return 0;
- }
写的代码和python版本相比,似乎长了点,但其实逻辑一致,得分应该和小P差不多,但我们现在,正在挣扎,要不要小C呢!为什么?因为同样作为初学者,同样一道题,作为一个C++的程序员,他写这道题,性能比Python那个版本,要来得差呢!我们把他的代码编译为速度优化版本,相同测试条件,连续跑了10次,最慢的要1秒多,最快也就600毫秒。远远比不上Python那个版本的281毫秒。如果要说优化,我们知道,用上PSYCO,代码基本上一行不改,就可再提速近100毫秒……
要拒掉小C,说他什么好?有人说,怪他不懂得使用API的优势,可以直接调用Windows API 的文件映射等技术,也可以不使用__stl_hash_string 这个由STL提供的字符串生成hash值的算法(埋怨人家小C数学学得不太好?);还可以埋怨他偷懒使用C++扩展库hash容器,其实只要1千行左右吧,应该可以写得了同个更高插入的查找性能的哈希表……(见此)
是的,这就是C++语言,我很不喜欢的某种“哲学”,高手总是可以有各种牛逼哄哄的手段,来写出相当不一般的,相当可以和初学者拉开令人“敬佩”的距离……
这可不是我喜欢的! 如果C++语言的学习,就是非得把一个初学者变成一个专家(,然后他才算入了门) 就已经够雷人了,那在库的使用上,我真不想逼着每个人都精通这个算法那个算法,都精通无数个库,都精通Windows或Linux那无数个API——哈哈,其这我也不是真正的教育家,这样说得有些沉重了。
真正的事实通常是这样,我,一个主要用C++的程序员,和我的那位玩Python的同事(他在工作中基本上不编程),下班后喝点小酒,然后扯到这件性能PK的事来,你说,我出手拿出一段数百,甚至上千行的代码,去PK人家的简洁优雅的20行代码。我这哪好意思嘛,简直就是未经先乱了阵脚。再者,人家的代码拿到linux下一回车,就能执行,你说我好意思还要在千行代码,非常wei suo地藏了几行Windows的API调用吗?
输就输了,但还是要在可接受的范围内,做点优化——Python俺也不是完全不懂,动态语言的一些内幕,我也略知一二的说。呵呵。这个可接受的范围,我约定的是,一不调用特定的操作系统的API,二是在C++环境内搞定。
- int main()
- {
- clock_t beg = clock();
- //优化一:给hastable预设一个足够大的数目
- hash_map<string, int, string_hash, string_compare> hastable(40050);
- ifstream fi ("test.txt");
- istringstream ssi;
- fi.seekg(0, ifstream::end);
- size_t fileSize = fi.tellg();
- fi.seekg(0);
- //优化二:一次读入文件的全部内容到内存
- char* buf = new char[fileSize];
- fi.read(buf, fileSize);
- //优化三:从存有数据的那段内存,直接构造出"内存流", 避免复制
- ssi.rdbuf()->pubsetbuf(buf, fi.gcount());
- fi.close();
- string line;
- size_t count = 0;
- ostringstream sso;
- //优化四:从内存流读每一行,而不是从文件流
- for(getline(ssi, line); !ssi.eof(); getline(ssi, line))
- {
- if (hastable.find(line) == hastable.end())
- {
- hastable[line] = 1;
- //优化五:先输出到内存流,而不是到文件流
- sso << line << endl;
- ++count;
- }
- }
- delete [] buf;
- ofstream fo ("test_dst.txt");
- fo << sso.str() << endl;
- fo.close();
- clock_t end = clock();
- cout << "count = " << count << ", time = " << (end - beg) << "ms" << endl;
- return 0;
- }
取文件大小,没有考虑超过2G的文件,我想在本“面试”中,是可以接受的。另一处要推敲的是,clock_t在linux下,会大很多,可以通过一个CLOCK_PER_SEC(?)宏解决,这里略。
1. 预定hash表个数
哈希表上来就先预定4万多个(差不多就是受测文件内容不同的行数),这有点赖皮呵呵。(或许你的hash没这功能)
2.一次读入文件全部内容
一次性读入文件,这可以理直气壮,估计py的C语言实现,也会玩这个。
3. 将内存直接变成"流"
读入内存后,通过“pubsetbuf” 函数,直接将它变成一个流(stringstream),这也是必然的,可以避免一次大内存申请,更主要是是内存复制。我相信对于多数语言,甚至同样是静态的Java语言,这都是此类需求的必然的实现方法。
4. 从内存流读,而不是从文件流
这也是第3步的目标。
5. 先输出到内存,最后再一次输出成文件。
这应该是最先想到的,而且效果可能也是最好的一个。
----------------------------------
就这样,还是比python慢:最快时是296秒。几个千分之一秒的差距嘛,呵呵……同事要祭出Psyco了。
我该怎么办呢?没事,我可以说,输赢不是目的,互相学习,理解不同语言间的机制,才是重要的——有点官腔的说——这样说吧,我的C++水平很一般……会有人有更漂亮而优雅的优化的。
-补充:-----------最后的优化:升级编译器 :) ----------------------------------------------------------
下面评论中有网友说,他只是优化了读写IO,用的是tr1(Technical Report 1,有待以后正式加入)下的hash_map,速度很是快了些了。非常感谢他的提醒,我们不妨试试同样是简单的(我不准备在这个小游戏上,大动干戈地改代码),但有效的性能优化之最后招数:我的Python同事有Psyco不可怕,我们坐着就可以享受编译器的升级。
我想起我用的编译器是非常古老mingw32-gcc3.4.5,费点劲,从网上下载了新版装上(4.4.0,好像也不是最新的gcc)。
一行代码不改,从296,直接提升为187 :) 好,现在C++完胜未优化版本的Python代码,可以和Psyco的版本比比了。
--------------------------------------------------------------------
最主要的是,性能和优雅,有时候后者更重要,不然的话,我也不会跟着玩了python这么些年,它性能差的地方,可多了。但它真的优雅。直到我发现,其实 除非逼急了,否则C++ 你也可以一直用得很优雅。而且优雅不是个人的事,当一个团队的人都保持优雅了,事情就美妙了。
换一句通俗的原则:千万别一开始就想着性能优化的事,所以,小C我们要了。
PS:
hash_map在vs下使用和在gcc下使用是有差异的,如下:
1.vs中:
#include <hash_map>
// 注意头文件和namespace
using namespace stdext;
int main()
{
hash_map<string, int> hmap;
return 0;
}
2.gcc下:
#include <ext/hash_map>
using namespace __gnu_cxx;
// 需要自己写hash函数
struct string_hash
{
size_t operator()(const string& str) const
{
return __stl_hash_string(str.c_str());
}
};
int main()
{
hash_map<string, int, string_hash> hmap;
return 0;
}
另外,在vs中使用hash_map还要特别注意待哈希对象的hash函数和comp函数的定义形式。
举个例子,对于我的自定义类型
typedef struct StatusNode
{
int data[3];
StatusNode* next;
StatusNode* parent;
}StatusNode,StatusList;
在gcc下,我们对StatusNode作hash所作的工作如下:
struct SN_hash
{
size_t operator()(const StatusNode& sn) const
{
return sn.data[0]+sn.data[1]*100+sn.data[2]*10000;
};
};
struct SN_comp
{
bool operator()(const StatusNode& sn1,const StatusNode& sn2) const
{
return (sn1.data[0]+sn1.data[1]*100+sn1.data[2]*10000)==(sn2.data[0]+sn2.data[1]*100+sn2.data[2]*10000);
};
};
然后
hash_map<StatusNode, int, SN_hash, SN_comp> hmap;
即可正常使用
但在vs下,你会发现这种作法是行不通的,取而代之的方法如下:(hash函数和comp函数定义在同一个struct中)
struct SN_hashmap
{
static const size_t bucket_size = 4;
static const size_t min_buckets = 8;
size_t operator()(const StatusNode& sn) const
{
return size_t(sn.data[0]+sn.data[1]*100+sn.data[2]*10000);
}
bool operator()(const StatusNode& sn1,const StatusNode& sn2) const
{
return (sn1.data[0]+sn1.data[1]*100+sn1.data[2]*10000)==(sn2.data[0]+sn2.data[1]*100+sn2.data[2]*10000);
}
};
然后
hash_map<StatusNode, int, SN_hashmap> hmap;
使用