Performance Haskell和可变结构';演出

Performance Haskell和可变结构';演出,performance,haskell,hashtable,mutable,Performance,Haskell,Hashtable,Mutable,我正在研究中给出的答案,并注意到与Python相比,使用一个小的输入确实会导致更快的Haskell运行 但随着数据集规模的扩大,Python占据了领先地位。使用基于hashmap的版本提高了性能,但仍然落后 更糟糕的是,我尝试将Python的字典翻译成哈希表,结果发现性能受到了严重影响。我真的很想了解发生了什么,因为我需要为未来的应用程序提供可变结构 下面是稍加修改的Python代码: #! /usr/bin/env python2.7 import random import re impor

我正在研究中给出的答案,并注意到与Python相比,使用一个小的输入确实会导致更快的Haskell运行

但随着数据集规模的扩大,Python占据了领先地位。使用基于hashmap的版本提高了性能,但仍然落后

更糟糕的是,我尝试将Python的字典翻译成哈希表,结果发现性能受到了严重影响。我真的很想了解发生了什么,因为我需要为未来的应用程序提供可变结构

下面是稍加修改的Python代码:

#! /usr/bin/env python2.7
import random
import re
import cPickle

class Markov:
    def __init__(self, filenames):
        self.filenames = filenames
        self.cache = self.train(self.readfiles())
        picklefd = open("dump", "w")
        cPickle.dump(self.cache, picklefd)
    print "Built a db of length "+str(len(self.cache))
        picklefd.close()

    def train(self, text):
        splitted = text.split(' ')
        print "Total of %d splitted words" % (len(splitted))
        cache = {}
        for i in xrange(len(splitted)-2):
            pair = (splitted[i], splitted[i+1])
            followup = splitted[i+2]
            if pair in cache:
                if followup not in cache[pair]:
                    cache[pair][followup] = 1
                else:
                    cache[pair][followup] += 1
            else:
                cache[pair] = {followup: 1}
        return cache

    def readfiles(self):
        data = ""
        for filename in self.filenames:
            fd = open(filename)
            data += fd.read()
            fd.close()
        return data

Markov(["76.txt"])
Haskell,原始响应(train4),其hashmap变体(trainHM2)和哈希表音译(trainHT):

Python(2.7)

Haskell(GHC 7.4.1)-“第4列”

Haskell-“列车HM2”

Haskell—“trainHT”-使用基本变体(我想这与Python对字典的作用很接近?)

两个表都使用线性或布谷鸟

0:06.06
0:05.69
最外面的桌子是布谷鸟,里面是线性的

0:04.17
分析表明有相当多的GC,因此,使用+RTS-A256M

0:02.11
对于输入的数据,我选择了其中一个答案,并将全文复制了12次。它应该达到7兆比特左右

测试是在一个VirtualBox容器中的Ubuntu12.04上运行的,使用一个i5-520M内核。做了不止一次,所有的结果都非常接近


最后一个结果对于这个微基准来说很好,但是考虑到以下情况,代码中还有什么需要改进的地方吗

  • Buckoo&Linear可能更适合此数据集,但“通用”Python解决方案在这方面没有太多优化就可以了
  • <> Valgrind报告了C++和Python版本大约有代码< 60MBS < /COD>。而Haskell RTS报告的任何地方都是从代码< > 125Mbs/COD>(布谷和线性)到<代码> 409Mbs<代码>(基本,更大堆)的同一任务的内存。在生产环境中如此频繁地调整垃圾收集器不是有害的吗?是否可以重构代码以减少内存使用
更新:

我想“减少垃圾”是我想要的。我知道Haskell的工作方式与C++不一样,但我想知道是否可以减少命令式代码中产生的垃圾,因为C++示例占用了内存的一半,没有任何空间泄漏。它有望在内存使用和执行时间方面有所改进(因为GC会减少)

更新2:

在表构造过程中计算长度确实减少了内存占用(实际上减少到大约
40MBs
),这会导致GC花费更长的时间,从而导致执行时间变慢(我想是因为丢弃了从列表中懒洋洋读取的值吧?)


是的,哈希表的操作需要花费大量的时间。我将试着模仿修改,看看是否有进一步的改进。

这并不是一个真正的答案,但太多了,无法在评论中添加,所以我将把它放在这里,直到更好的东西出现。我看不出你的hashtable代码有任何明显的错误(这是我真正研究的唯一部分),除了一些重构/打高尔夫球

首先,内存使用对我来说并不奇怪。使用
-A256M
,您要求RTS的最小分配区域为256M,这样就可以降低内存使用率。如果在GC之后升级或复制数据,内存使用量将增加。还请注意,与其他语言相比,Haskell数据结构往往有点内存不足,请参见示例。考虑到这两个因素,我对大分配区域的总内存使用率并不感到惊讶

像HashMap或bytestring trie这样的结构可能会占用更少的内存,使用hashtable以外的数据结构也会带来负面影响

说到分配区域,这段代码有点像微基准,因为几乎所有分配的数据(主要是bytestring数据和内部哈希表值)都是长寿命的(它们持续到程序结束)。这将使您的测试程序处于一种非常大的分配区域特别有利的情况下,而如果此数据库构建只是更大程序的一部分,则更大区域的成本可能会占主导地位

至于生产环境的最佳GC设置,在实际完整程序的上下文之外很难判断。我可以说,如果性能真的很重要,那么花一些时间调整GC标志是值得的。如果启用了线程运行时,情况更是如此


除了内存问题,我强烈怀疑hashtables包在这里对您不利。根据配置文件,成本最高的4个功能是
lookup/go
lookup
insert
,和
delete'.go
。我认为,如果它具有与
Data.Map.alter
等价的功能,您的一些操作就可以合并在一起以获得性能增益。如果Python字典没有针对
cache[key]+=1这样的情况进行优化,我会非常惊讶。

“代码中还有什么需要改进的地方”是一个大问题。你能说得更具体些吗?你说有很多GC,但是不要说太多关于你从分析中学到的东西,或者出现了什么问题。你远远没有给出完整的答案,但是你通过打印长度来强制整个单词列表,并将其保留在内存中以供dict构造。通过不打印长度,我节省了大约100米的基本、更大的堆大小。如果需要,可以在构建字典的同时生成一个长度值。
$ /usr/bin/time -f "%E" ./pytest.py 
Total of 1255153 splitted words
Built a db of length 64442
0:02.62
$ 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
$ /usr/bin/time -f "%E" ./hasktest
1255153
"Built a DB of 64442 words"
0:04.23
$ /usr/bin/time -f "%E" ./hasktest
1255153
"Built a DB"
0:10.42
0:06.06
0:05.69
0:04.17
0:02.11