Implement an optimized Cache::Entry coder by casperisfine · Pull Request #42025...
source link: https://github.com/rails/rails/pull/42025
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Active Support's cache have for long been limited because of its format. It directly serialize its Entry
object with Marshal
, so any internal change might break the format.
The current shortcomings are:
- The minimum entry overhead is quite ridiculous:
Marshal.dump(ActiveSupport::Cache::Entry.new("")).bytesize # => 107
- Only the internal
value
is compressed, but unless it's a String, to do so it first need to be serialized. So we end up withMarshal.dump(Zlib.deflate(Marshal.dump(value)))
which is wasteful. - Overall the format can't be evolved which prevent many refactorings.
I previously submitted #40412 which went quite far to have a compact header, etc. But ultimately I think the extra complexity isn't quite justified, the only prefix we need is a single byte to know the format. Right now it uses 0
for uncompressed payloads and 1
for gziped
ones. But that leaves over 200 other prefixes for future extensions.
This change is supposed to be fully backward and forward compatible, as long as Rails 7.0 with load_defaults '6.1'
is deployed first, and then set to load_defaults '7.0'
in a later deploy.
Performance
Overall this new implementation is substantially faster than the old one, and also always produce smaller payloads. Of course the larger the cached values, the smaller the difference matters.
But more importantly it opens the door to further improvements, thanks to the version prefix.
# frozen_string_literal: true require 'benchmark/ips' require 'active_support/all' require 'zlib' sizes = if ENV['SIZES'] ENV['SIZES'].split(',').map(&:to_i) else [0, 10, 2_000] end sizes.each do |bytesize| puts "================ #{bytesize} bytes ================" if bytesize == 0 entry_marshal = Marshal.dump(ActiveSupport::Cache::Entry.new('')) entry_pack = ActiveSupport::Cache::EntryCoder.dump(ActiveSupport::Cache::Entry.new('', compress: false)) else string = ('A'..'z').cycle.take(bytesize).join entry_marshal = Marshal.dump(ActiveSupport::Cache::Entry.new(string, expires_in: 1.hour, version: "v42").compressed(1_024)) entry_pack = ActiveSupport::Cache::EntryCoder.dump_compressed(ActiveSupport::Cache::Entry.new(string, expires_in: 1.hour, version: "v42", compress: false), 1_024) end puts "Marshal.dump.bytesize: #{entry_marshal.bytesize}" puts "EntryCoder.dump.bytesize: #{entry_pack.bytesize}" puts Benchmark.ips do |x| x.report('Marshal.dump') { Marshal.dump(ActiveSupport::Cache::Entry.new(string, expires_in: 1.hour, version: "v42").compressed(1_024)) } x.report('EntryCoder.dump') { ActiveSupport::Cache::EntryCoder.dump_compressed(ActiveSupport::Cache::Entry.new(string, expires_in: 1.hour, version: "v42", compress: false), 1_024) } x.compare! end puts Benchmark.ips do |x| x.report('Marshal.load') { Marshal.load(entry_marshal).value } x.report('EntryCoder.load') { ActiveSupport::Cache::EntryCoder.load(entry_pack).value } x.compare! end puts puts end
================ 0 bytes ================
Marshal.dump.bytesize: 90
EntryCoder.dump.bytesize: 13
Warming up --------------------------------------
Marshal.dump 17.030k i/100ms
EntryCoder.dump 20.436k i/100ms
Calculating -------------------------------------
Marshal.dump 168.942k (± 1.0%) i/s - 851.500k in 5.040696s
EntryCoder.dump 205.791k (± 1.3%) i/s - 1.042M in 5.065456s
Comparison:
EntryCoder.dump: 205790.5 i/s
Marshal.dump: 168942.0 i/s - 1.22x (± 0.00) slower
Warming up --------------------------------------
Marshal.load 35.907k i/100ms
EntryCoder.load 50.413k i/100ms
Calculating -------------------------------------
Marshal.load 357.504k (± 2.2%) i/s - 1.795M in 5.024558s
EntryCoder.load 501.540k (± 0.8%) i/s - 2.521M in 5.026151s
Comparison:
EntryCoder.load: 501539.7 i/s
Marshal.load: 357504.0 i/s - 1.40x (± 0.00) slower
================ 10 bytes ================
Marshal.dump.bytesize: 128
EntryCoder.dump.bytesize: 52
Warming up --------------------------------------
Marshal.dump 14.352k i/100ms
EntryCoder.dump 19.461k i/100ms
Calculating -------------------------------------
Marshal.dump 144.208k (± 0.8%) i/s - 731.952k in 5.075996s
EntryCoder.dump 194.288k (± 0.8%) i/s - 973.050k in 5.008606s
Comparison:
EntryCoder.dump: 194287.7 i/s
Marshal.dump: 144208.3 i/s - 1.35x (± 0.00) slower
Warming up --------------------------------------
Marshal.load 28.966k i/100ms
EntryCoder.load 40.911k i/100ms
Calculating -------------------------------------
Marshal.load 289.728k (± 0.7%) i/s - 1.477M in 5.099033s
EntryCoder.load 406.628k (± 0.7%) i/s - 2.046M in 5.030772s
Comparison:
EntryCoder.load: 406628.0 i/s
Marshal.load: 289727.6 i/s - 1.40x (± 0.00) slower
================ 2000 bytes ================
Marshal.dump.bytesize: 221
EntryCoder.dump.bytesize: 128
Warming up --------------------------------------
Marshal.dump 3.940k i/100ms
EntryCoder.dump 3.999k i/100ms
Calculating -------------------------------------
Marshal.dump 39.468k (± 0.9%) i/s - 200.940k in 5.091648s
EntryCoder.dump 39.909k (± 0.9%) i/s - 199.950k in 5.010555s
Comparison:
EntryCoder.dump: 39909.0 i/s
Marshal.dump: 39467.7 i/s - same-ish: difference falls within error
Warming up --------------------------------------
Marshal.load 10.632k i/100ms
EntryCoder.load 13.317k i/100ms
Calculating -------------------------------------
Marshal.load 105.531k (± 1.0%) i/s - 531.600k in 5.037931s
EntryCoder.load 134.306k (± 1.3%) i/s - 679.167k in 5.057798s
Comparison:
EntryCoder.load: 134305.7 i/s
Marshal.load: 105531.0 i/s - 1.27x (± 0.00) slower
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK