Ruby 一个 String 类型处理不当导致的性能问题,在导出 CSV 文件等场景不注意就会中招

gazeldx · 2015年12月16日 · 最后由 catherine 回复于 2015年12月18日 · 3861 次阅读

一种的生成 csv 文件的做法:

csv = ''
@customers.each do |customer|
  csv = "#{csv}#{customer.id},#{.......}\n"
end
return csv

这种做法存在性能问题,特别是 customers 达到 1 万条以上,并且每一行内容比较长的时候。 原因是 csv 这个字符串变量太大了(达到 1M),再往 csv 的末尾追加内容会比较吃力,这种情况,生成 csv 最终结果要 1 分钟。

解决的办法:

csv = []
@customers.each do |customer|
  csv << "#{customer.id},#{.......}"
end
return csv.join("\n")

csv 从字符串变成了 Array, 避免了超大 String 的可能。和上述同样场景下,生成 csv 最终结果只要 5 秒。

👍好棒。

感觉写过似曾相识的代码

@pathbox 哈哈,写过 Java 的同学可能都上过这么一课:不要在循环中用+链接字符串,要在循环中链接字符串应该使用StringBuilderStringBuffer。原因就是 String 是 immutable 对象,每次修改操作会创建一个 String 对象。

在 Ruby 中试验了一下,发现 =#{} 这操作也是会创建新对象。

csv = ''
csv.object_id  # 12888796780
3.times{csv = "#{csv} Hello"; puts csv.object_id}
# 12888886920
# 12888886840
# 12888886780

所以 OP 说

csv 这个字符串变量太大了(达到 1M),再往 csv 的末尾追加内容会比较吃力

只说对了一半,还有一个原因是循环到后面,每次循环都要创建一个 1M 多的字符串然后再往追加内容这样才慢(在 String 末尾追加应该是 O(1) )。如果这时看一下内存占用应该很可观,想必上 G 没问题。

不过 Ruby 的 String 还有在原 String 对象上连接的方法,就是 << 。所以 OP 的代码还能这样写

csv = ''
@customers.each do |customer|
  csv << "#{customer.id},#{.......}\n"
end
return csv[0..-2]

对 Ruby 不熟,这只是我想当然而已,别打脸。

#4 楼 @DouO 有篇文章 千万别建超过 24 个字符的字符串

用 map 就不会掉坑了

@customers.map{|customer| "#{customer.id},#{.......}"}.join("\n")

add to_csv method into customer model then do @customers.map(&:to_csv).join("\n")

第一种写法看着就挺奇怪的。

所以一直坚持用函数式做法……

#6 楼 @quakewang 把过多的数据保存在 ruby 对象里都不好,为什么不直接追加到文件里?

太棒了!之前做了个导入功能,测试完发布了。反馈就是说太耗时。救星啊!!

13 楼 已删除
需要 登录 后方可回复, 如果你还没有账号请 注册新账号