我正在研究Optimizing Haskell code中給出的答案,並注意到與Python相比,使用小輸入確實會導致更快的Haskell運行。Haskell和可變結構的性能
但隨着數據集的規模不斷擴大,Python佔據了領先地位。使用基於散列表的版本提高了性能,但仍然落後。
更糟糕的是,我嘗試將Python的字典轉換爲哈希表,並觀察到硬性能。我真的很想知道發生了什麼,因爲我需要爲將來的應用程序使用可變結構。
這裏的稍微修改的Python代碼:
3210Haskell中,與原來的響應(train4),一個HashMap變體(trainHM2)和散列表音譯(trainHT):
{-# LANGUAGE BangPatterns,DeriveGeneriC#-}
import GHC.Generics (Generic)
import Data.List (foldl')
import Data.Hashable
import qualified Data.Map as M
import qualified Data.HashMap.Strict as HM
import qualified Data.ByteString.Char8 as B
import qualified Data.HashTable.IO as HT
--Using this instead of tuples yielded a 5~10% speedup
data StringTuple = STP !B.ByteString !B.ByteString deriving(Ord,Eq,Generic)
instance Hashable StringTuple
type Database3 = M.Map StringTuple (M.Map B.ByteString Int)
type DatabaseHM = HM.HashMap StringTuple (HM.HashMap B.ByteString Int)
type DatabaseHT = HT.BasicHashTable StringTuple DatabaseInnerHT
type DatabaseInnerHT = (HT.BasicHashTable B.ByteString Int)
train4 :: [B.ByteString] -> Database3
train4 words = foldl' update M.empty (zip3 words (drop 1 words) (drop 2 words))
where update m (x,y,z) = M.insertWith' (inc z) (STP x y) (M.singleton z 1) m
inc k _ = M.insertWith' (+) k 1
trainHM2 :: [B.ByteString] -> DatabaseHM
trainHM2 words = trainHM2G words HM.empty
where
trainHM2G (x:y:[]) !hm = hm
trainHM2G (x:y:z:rem) !hm = trainHM2G (y:z:rem) (HM.insertWith (inc z) (STP x y) (HM.singleton z 1) hm)
where inc k _ = HM.insertWith (+) k 1
trainHT :: [B.ByteString] -> IO (DatabaseHT)
trainHT words = do
hm <- HT.new
trainHT' words hm
where
trainHT' (x:y:[]) !hm = return hm
trainHT' (x:y:z:rem) !hm = do
let pair = STP x y
inCache <- HT.lookup hm pair
case inCache of
Nothing -> do
htN <- HT.new :: IO (DatabaseInnerHT)
HT.insert htN z $! 1
HT.insert hm pair $! htN
Just ht -> do
cvM <- HT.lookup ht z
case cvM of
Nothing -> HT.insert ht z 1
Just cv -> HT.insert ht z $! (cv+1)
trainHT' (y:z:rem) hm
main = do contents <- B.readFile "76.txt"
let bcont = B.split ' ' $ contents
print $ length bcont
let db = train4 $ bcont
print $ "Built a DB of " ++ show (M.size db) ++ " words"
--let db = trainHM2 $ bcont
--print $ "Built a DB of " ++ show (HM.size db) ++ " words"
--db <- trainHT $ (bcont)
--print $ "Built a DB"
臨時C++ 11音譯(需要-fpermissive編譯,感覺自由糾正它):
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_map>
#include <tuple>
/*
Hash stuff here
Taken from https://stackoverflow.com/a/7111460/314327
*/
size_t hash_combiner(size_t left, size_t right) //replacable
{ return left^right;}
template<int index, class...types>
struct hash_impl {
size_t operator()(size_t a, const std::tuple<types...>& t) const {
typedef typename std::tuple_element<index, std::tuple<types...>>::type nexttype;
hash_impl<index-1, types...> next;
size_t b = std::hash<nexttype>()(std::get<index>(t));
return next(hash_combiner(a, b), t);
}
};
template<class...types>
struct hash_impl<0, types...> {
size_t operator()(size_t a, const std::tuple<types...>& t) const {
typedef typename std::tuple_element<0, std::tuple<types...>>::type nexttype;
size_t b = std::hash<nexttype>()(std::get<0>(t));
return hash_combiner(a, b);
}
};
namespace std {
template<class...types>
struct hash<std::tuple<types...>> {
size_t operator()(const std::tuple<types...>& t) {
const size_t begin = std::tuple_size<std::tuple<types...>>::value-1;
return hash_impl<begin, types...>()(1, t); //1 should be some largervalue
}
};
}
/*
Hash stuff end
*/
using namespace std;
/*
Split, from https://stackoverflow.com/a/236803/314327
*/
vector<string> &split(const string &s, char delim, vector<string> &elems) {
stringstream ss(s);
string item;
while (getline(ss, item, delim)) {
elems.push_back(item);
}
return elems;
}
vector<string> split(const string &s, char delim) {
vector<string> elems;
split(s, delim, elems);
return elems;
}
/*
Split end
*/
typedef tuple<string,string> STP;
unordered_map< STP,unordered_map< string,int > > train(vector<string> &words)
{
unordered_map< STP,unordered_map< string,int > > cache;
for(int i=0;i<words.size()-2;i++)
{
STP tup = make_tuple(words[i],words[i+1]);
auto it = cache.find(tup);
if(it!=cache.end())
{
auto it2 = it->second.find(words[i+2]);
if(it2!=it->second.end())
{
it2->second += 1;
}
else
it->second[words[i+2]] = 1;
}
else
{
unordered_map< string,int > cacheInner;
cacheInner[words[i+2]] = 1;
cache[tup] = cacheInner;
}
}
return cache;
}
int main()
{
ifstream ifs("76.txt");
stringstream buf;
buf << ifs.rdbuf();
string contents(buf.str());
auto words = split(contents,' ');
cout << words.size();
auto wordCache = train(words);
cout << "\nHashtable count " << wordCache.size();
cout << "\n";
return 0;
}
並且結果:
C++(GCC 4.6.3)
$ g++ -O3 -fpermissive -std=c++0x cpptest.cpp -o cpptest
$ /usr/bin/time -f "%E" ./cpptest
1255153
Hashtable count 64442
0:01.02
的Python(2.7)
$ /usr/bin/time -f "%E" ./pytest.py
Total of 1255153 splitted words
Built a db of length 64442
0:02.62
的Haskell(GHC 7.4 .1) - 「train4」
$ ghc -fllvm -O2 -rtsopts -fforce-recomp -funbox-strict-fields hasktest.hs -o hasktest
[1 of 1] Compiling Main (hasktest.hs, hasktest.o)
Linking hasktest ...
$ /usr/bin/time -f "%E" ./hasktest
1255153
"Built a DB of 64442 words"
0:06.35
哈斯克爾 - 「trainHM2」
$ /usr/bin/time -f "%E" ./hasktest
1255153
"Built a DB of 64442 words"
0:04.23
哈斯克爾 - 「trainHT」 - (?這是接近到什麼Python不爲字典,我猜)使用基本型
$ /usr/bin/time -f "%E" ./hasktest
1255153
"Built a DB"
0:10.42
使用線性或杜鵑兩個表
0:06.06
0:05.69
使用杜鵑用於最外層表和線性在裏面
0:04.17
剖析表明,有相當多的GC的,所以,用+ RTS -A256M
0:02.11
輸入數據,我選擇了76.txt作爲答案的一個指示和複製的全文12次。它應該達到約7 MB。
測試是在一個VirtualBox容器中的Ubuntu 12.04上運行的,使用一個i5-520M內核。不止一次運行,所有結果都非常接近。
最後的結果是這個微基準精緻漂亮,但還有什麼代碼改善,考慮到:
- 杜鵑&線性可能更適合這個數據集,但「通用」Python解決方案在這方面沒有多少優化就很好,Valgrind報告稱C++ & Python版本大約需要
60MBs
,而Haskell RTS從125MBs
(Cuckoo & Linear)至409MBs
(基本的,較大的堆)的內存用於同一任務。不會在生產環境中調整垃圾收集器是否有害?是否有可能重構代碼,所以它的內存使用量更少?
更新:
我猜 「減少垃圾」 就是我要找的。我知道Haskell不能像C++那樣工作,但我想知道是否有可能減少命令代碼中產生的垃圾,因爲C++示例在沒有任何空間泄漏的情況下消耗了一半內存。它希望是內存使用和執行時間方面的改進(因爲GC會更少)。
更新2:
計算表施工期間的長度減少了內存佔用肯定(下降到大約40MBs
,實際上!),這將導致GC需要更長的時間,導致較慢的執行時間(由於丟棄了從列表中懶得讀出的值,我認爲?)。
是的,哈希表的操作需要很長時間。我會嘗試模仿改動,看看它是否有進一步改善。
「還有什麼需要改進的代碼」是一個很大的問題。你可以說得更詳細點嗎?你說有很多GC,但是別說你從分析中學到了什麼,或者出現了什麼問題。 – jberryman
遠不是一個完整的答案,但是你通過打印長度來強制整個單詞列表,並保留它在字典構造的內存中。通過不打印長度,我節省了大約100M的基本,更大的堆大小。如果你需要它,你可以在字典構造的同時創建一個長度值。 –