Thao tác với tệp có dung lượng lớn trong Ruby
Bài đăng này đã không được cập nhật trong 6 năm
Làm việc với những file dữ liệu lớn, điển hình là CSV không phải là chuyện hiếm gặp. Với Ruby, có khá nhiều cách để xử lý thông tin những file này, nhưng hay cùng kiểm chứng xem tài nguyên hệ thống được tiêu tốn thế nào cho mỗi cách.
Khởi tạo môi trường
Ruby version : 2.4.0
Operation : macOS SierraSierra 10.12.5
Processor : 2.7 GHz Intel Core i5
Memory : 8 GB 1867 MHz DDR3
Tạo file helpers.rb chứa method hiển thị thời gian và lượng bộ nhớ sử dụng :
require "benchmark"
def print_memory_usage
memory_before = `ps -o rss= -p #{Process.pid}`.to_i
yield
memory_after = `ps -o rss= -p #{Process.pid}`.to_i
puts "Memory: #{((memory_after - memory_before) / 1024.0).round(2)} MB"
end
def print_time_spent
time = Benchmark.realtime do
yield
end
puts "Time: #{time.round(2)}"
end
Sau đó thử 1 file csv khoảng trên dưới 1 triệu dòng.
require "csv"
require_relative "./helpers"
headers = ["id", "name", "email"]
name = "Elijah Mikaelson"
email = "elijah@mikaelson.com"
print_memory_usage do
print_time_spent do
CSV.open("data.csv", "w", write_headers: true, headers: headers) do |csv|
1_000_000.times do |i|
csv << [i, name, email]
end
end
end
end
Với máy mình, đây là thông số được trả về khi tạo file csv trên :
Time: 4.69
Memory: 0.14 MB
File data.csv tạo ra có dung lượng khoảng 43 MB.
Sử dụng CSV.read để đọc cả file
require_relative './helpers'
require 'csv'
print_memory_usage do
print_time_spent do
csv = CSV.read('data.csv', headers: true)
sum = 0
csv.each do |row|
sum += row['id'].to_i
end
puts "Sum: #{sum}"
end
end
Sum: 499999500000
Time: 10.85
Memory: 626.81 MB
Xử lý file csv có 43 MB mà đã dùng tới hơn 600 MB, điều này thật sự là không ổn. Điều này xảy ra do có quá nhiều String object được tạo ra, và không được dọn dẹp ngay sau khi đã sử dụng.
Sử dụng CSV.parse
Lần này ta đọc file csv sau đó khởi tạo thành một CSV object để sử dụng.
require_relative './helpers'
require 'csv'
print_memory_usage do
print_time_spent do
content = File.read('data.csv')
csv = CSV.parse(content, headers: true)
sum = 0
csv.each do |row|
sum += row['id'].to_i
end
puts "Sum: #{sum}"
end
end
Sum: 499999500000
Time: 11.1
Memory: 638.34 MB
Hiển nhiên bộ nhớ tiêu tốn tăng lên do chứa cả object CSV ta vừa parse được.
Xử lý từng dòng của file
require_relative './helpers'
require 'csv'
print_memory_usage do
print_time_spent do
content = File.read('data.csv')
csv = CSV.new(content, headers: true)
sum = 0
while row = csv.shift
sum += row['id'].to_i
end
puts "Sum: #{sum}"
end
end
Sum: 499999500000
Time: 9.27
Memory: 42.85 MB
Đã thấy sự khác biệt, với cách đọc từng dòng này, ta chỉ tốn bộ nhớ lưu lại CSV object, kích thước tương đương dung lượng file csv thực tế.
Xử lý từng dòng của file từ IO object
require_relative './helpers'
require 'csv'
print_memory_usage do
print_time_spent do
File.open('data.csv', 'r') do |file|
csv = CSV.new(file, headers: true)
sum = 0
while row = csv.shift
sum += row['id'].to_i
end
puts "Sum: #{sum}"
end
end
end
Sum: 499999500000
Time: 9.93
Memory: 0.23 MB
Bạn có thấy gì không, chỉ 0.23 MB thôi ?
Hoặc ta có thể dùng CSV.foreach
require_relative './helpers'
require 'csv'
print_memory_usage do
print_time_spent do
sum = 0
CSV.foreach('data.csv', headers: true) do |row|
sum += row['id'].to_i
end
puts "Sum: #{sum}"
end
end
Sum: 499999500000
Time: 9.65
Memory: 0.2 MB
Để tạo ra sự khác biệt này, theo mình khi sử dụng IO object, thực chất ta đang stream nội dung của object, dùng tới đâu load tới đó chứ không phải load tất cả nội dung file vào bộ nhớ, từ đó dẫn tới sự khác biết hoàn toàn bộ nhớ.
Kết luận
Phần lớn các trường hợp ta xử lý file csv lớn đều không cần load tất cả vào bộ nhớ làm gì, vì vậy ta hoàn toàn có những cách để xử lý để tiết kiệm tài nguyên hệ thống triệt để.
All rights reserved