3

Implement an optimized Cache::Entry coder by casperisfine · Pull Request #42025...

 2 years ago
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.

Copy link

Contributor

casperisfine commented on Apr 20

edited

Ref: #40412
Ref: #9494

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 with Marshal.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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK