Python 多线程性能 - 改用 C++?
Posted
技术标签:
【中文标题】Python 多线程性能 - 改用 C++?【英文标题】:Python multithreading performance - use C++ instead? 【发布时间】:2014-06-06 16:21:23 【问题描述】:所以,我有一个 Python 脚本,它基本上可以编写一个超过 80GB 的文件。目前它只是串行运行,我唯一一次实际运行它需要大约 13 个小时才能在服务器上运行。
我将并行化它,以便它写入许多文件,而不仅仅是一个。
将我已经拥有的东西保存在 Python 中会稍微容易一些,但要合并多个线程(他们需要访问一个共享数据的单个映射,没有人会写入,所以它不会需要保护)。
但是,将其保留在 Python 中是否愚蠢?我也知道 C++,所以你认为我应该用 C++ 重写它吗?我认为该程序比其他任何程序都更受磁盘限制(没有用于写入文件的大量逻辑),所以它可能没有太大区别。我不确定 C++ 需要多长时间来编写相同的 80GB 文件(串行)。
2014 年 6 月 6 日更新,太平洋标准时间 16:40: 我在下面发布我的代码以确定代码本身是否存在瓶颈,而不是纯粹的磁盘绑定。
我为每个表调用一次 writeEntriesToSql(),其中大约有 30 个表。 “大小”是要写入表的插入数。所有表的累积大小约为 200,000,000。
我确实注意到我一遍又一遍地编译我的正则表达式,这可能会给我带来很多浪费,尽管我不确定有多少。
def writeEntriesToSql(db, table, size, outputFile):
# get a description of the table
rows = queryDatabaseMultipleRows(db, 'DESC ' + table)
fieldNameCol = 0 # no enums in python 2.7 :(
typeCol = 1
nullCol = 2
keyCol = 3
defaultCol = 4
extraCol = 5
fieldNamesToTypes =
for row in rows:
if (row[extraCol].find("auto_increment") == -1):
# insert this one
fieldNamesToTypes[row[fieldNameCol]] = row[typeCol]
for i in range(size):
fieldNames = ""
fieldVals = ""
count = 0
# go through the fields
for fieldName, type in fieldNamesToTypes.iteritems():
# build a string of field names to be used in the INSERT statement
fieldNames += table + "." + fieldName
if fieldName in foreignKeys[table]:
otherTable = foreignKeys[table][fieldName][0]
otherTableKey = foreignKeys[table][fieldName][1]
if len(foreignKeys[table][fieldName]) == 3:
# we already got the value so we don't have to get it again
val = foreignKeys[table][fieldName][2]
else:
# get the value from the other table and store it
#### I plan for this to be an infrequent query - unless something is broken here!
query = "SELECT " + otherTableKey + " FROM " + otherTable + " LIMIT 1"
val = queryDatabaseSingleRowCol(db, query)
foreignKeys[table][fieldName].append(val)
fieldVals += val
else:
fieldVals += getDefaultFieldVal(type)
count = count + 1
if count != len(fieldNamesToTypes):
fieldNames += ","
fieldVals += ","
# return the default field value for a given field type which will be used to prepopulate our tables
def getDefaultFieldVal(type):
if not ('insertTime' in globals()):
global insertTime
insertTime = datetime.utcnow()
# store this time in a file so that it can be retrieved by SkyReporterTest.perfoutput.py
try:
timeFileName = perfTestDir + "/dbTime.txt"
timeFile = open(timeFileName, 'w')
timeFile.write(str(insertTime))
except:
print "!!! cannot open file " + timeFileName + " for writing. Please make sure this is run where you have write permissions\n"
os.exit(1)
# many of the types are formatted with a typename, followed by a size in parentheses
##### Looking at this more closely, I suppose I could be compiling this once instead of over and over - a bit bottleneck here?
p = re.compile("(.*)\(([0-9]+).*")
size = 0
if (p.match(type)):
size = int(p.sub(r"\2", type))
type = p.sub(r"\1", type)
else:
size = 0
if (type == "tinyint"):
return str(random.randint(1, math.pow(2,7)))
elif (type == "smallint"):
return str(random.randint(1, math.pow(2,15)))
elif (type == "mediumint"):
return str(random.randint(1, math.pow(2,23)))
elif (type == "int" or type == "integer"):
return str(random.randint(1, math.pow(2,31)))
elif (type == "bigint"):
return str(random.randint(1, math.pow(2,63)))
elif (type == "float" or type == "double" or type == "doubleprecision" or type == "decimal" or type == "realdecimal" or type == "numeric"):
return str(random.random() * 100000000) # random endpoints for this random
elif (type == "date"):
insertTime = insertTime - timedelta(seconds=1)
return "'" + insertTime.strftime("%Y-%m-%d") + "'"
elif (type == "datetime"):
insertTime = insertTime - timedelta(seconds=1)
return "'" + insertTime.strftime("%Y-%m-%d %H:%M:%S") + "'"
elif (type == "timestamp"):
insertTime = insertTime - timedelta(seconds=1)
return "'" + insertTime.strftime("%Y%m%d%H%M%S") + "'"
elif (type == "time"):
insertTime = insertTime - timedelta(seconds=1)
return "'" + insertTime.strftime("%H:%M:%S") + "'"
elif (type == "year"):
insertTime = insertTime - timedelta(seconds=1)
return "'" + insertTime.strftime("%Y") + "'"
elif (type == "char" or type == "varchar" or type == "tinyblog" or type == "tinytext" or type == "blob" or type == "text" or type == "mediumblob"
or type == "mediumtext" or type == "longblob" or type == "longtext"):
if (size == 0): # not specified
return "'a'"
else:
lst = [random.choice(string.ascii_letters + string.digits) for n in xrange(size)]
strn = "".join(lst)
return strn
elif (type == "enum"):
return "NULL" # TBD if needed
elif (type == "set"):
return "NULL" # TBD if needed
else:
print "!!! Unrecognized mysql type: " + type + "\n"
os.exit(1)
【问题讨论】:
文件是ASCII码。 好吧,这在很大程度上取决于您的代码的功能!它有什么作用? 如果程序是 I/O-bound,你可能不会从切换到 C++ 中受益,但一定要注意适当的缓冲。 写入 80GB,即使是在慢速闪存驱动器上,也不会花费 13 个小时,因此很明显,大部分时间都被 I/O 以外的某种活动占用了。如果没有重要的网络组件(即 13 个小时的大部分时间没有花在等待对远程系统的查询上),那么您可能受到 CPU 和/或内存的限制。如果您的 CPU 受限并且有多个 CPU,线程可能会有所帮助。但是,如果您的内存受限,线程可能会使情况变得更糟。有一些代码可以看可能会很好...... 如果您 profile 您的代码可以准确查看时间花费在哪里,这将有所帮助。您很可能会在 Python 的 I/O 之外的令人惊讶的地方发现瓶颈。 【参考方案1】:Python 的 I/O 并不比其他语言慢多少。解释器的启动速度可能会很慢,但编写这么大的文件会抵消这种影响。
我建议查看multiprocessing 模块,它可以让您通过拥有多个 Python 实例来获得真正的并行性,这将有助于绕过 GIL。但是,这些会附加一些开销,但同样,对于 80GB 的文件,它应该没有太大关系。请记住,每个进程都是一个完整的进程,这意味着它将占用更多的计算资源。
【讨论】:
【参考方案2】:另外请记住,根据您的配置,由于您的代码已经是 IO/Bound,您可能会获得低/无加速。如果您从多个线程向它写入单个磁盘,则弊大于利。
【讨论】:
以上是关于Python 多线程性能 - 改用 C++?的主要内容,如果未能解决你的问题,请参考以下文章