persistent_memoize 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +133 -0
- data/Rakefile +8 -0
- data/examples/example_fibonacci.rb +41 -0
- data/lib/persistent_memoize.rb +26 -0
- data/lib/persistent_memoize/version.rb +3 -0
- data/persistent_memoize.gemspec +35 -0
- data/test/test_memoize.rb +54 -0
- metadata +108 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2013 Neil Kandalgaonkar
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject
|
11
|
+
to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
20
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
21
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# PersistentMemoize
|
2
|
+
|
3
|
+
## Description
|
4
|
+
Speed up methods at the cost of disk space. Keep caches on disk to speed
|
5
|
+
up later invocations of your program.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'persistent_memoize'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install persistent_memoize
|
19
|
+
|
20
|
+
## Synopsis
|
21
|
+
|
22
|
+
Let's imagine we have a script, _fib.rb_:
|
23
|
+
|
24
|
+
def fib(n)
|
25
|
+
return n if n < 2
|
26
|
+
fib(n-1) + fib(n-2)
|
27
|
+
end
|
28
|
+
|
29
|
+
puts fib(40)
|
30
|
+
|
31
|
+
Executing it is slow - on my machine, this is 20 seconds!
|
32
|
+
|
33
|
+
$ time ruby fib.rb
|
34
|
+
102334155
|
35
|
+
|
36
|
+
real 0m20.107s
|
37
|
+
user 0m20.067s
|
38
|
+
sys 0m0.034s
|
39
|
+
|
40
|
+
So let's add memoization.
|
41
|
+
|
42
|
+
require 'persistent_memoize'
|
43
|
+
include PersistentMemoize
|
44
|
+
|
45
|
+
def fib(n)
|
46
|
+
return n if n < 2
|
47
|
+
fib(n-1) + fib(n-2)
|
48
|
+
end
|
49
|
+
|
50
|
+
memoize(:fib, './fib-cache')
|
51
|
+
puts fib(40)
|
52
|
+
|
53
|
+
And executing it now takes about a tenth of a second.
|
54
|
+
|
55
|
+
$ time ruby ./fib.rb
|
56
|
+
102334155
|
57
|
+
|
58
|
+
real 0m0.102s
|
59
|
+
user 0m0.062s
|
60
|
+
sys 0m0.036s
|
61
|
+
|
62
|
+
What's more, it stays fast - subsequent invocations after the first one are even faster.
|
63
|
+
|
64
|
+
$ time ruby ./fib.rb
|
65
|
+
102334155
|
66
|
+
|
67
|
+
real 0m0.092s
|
68
|
+
user 0m0.060s
|
69
|
+
sys 0m0.029s
|
70
|
+
|
71
|
+
## Motivation
|
72
|
+
|
73
|
+
This library is most useful when you have a program that runs repeatedly,
|
74
|
+
but which has to incorporate calculations or API results that rarely
|
75
|
+
change.
|
76
|
+
|
77
|
+
I use it to rebuild a static blog, which is based on my postings
|
78
|
+
on other sites, fetched via API. If I tweak the look of the blog, I don't want to have to
|
79
|
+
fetch all those postings all over again. So it's a quick modification
|
80
|
+
to the library that looks up those results, to cache them locally, via
|
81
|
+
memoization. If I know that those blog postings have changed, then I
|
82
|
+
delete the cache, manually, and then it regenerates from scratch.
|
83
|
+
|
84
|
+
## Caveats
|
85
|
+
|
86
|
+
As with all memoization, the method memoized must be _pure_, that
|
87
|
+
is, the result depends solely on its arguments. Memoizing a function which
|
88
|
+
returns the current date will give you the wrong answer tomorrow.
|
89
|
+
|
90
|
+
This can't magically make every method faster; it's all trade-offs. The
|
91
|
+
thing-to-be-memoized should be more expensive than computing a hash, and
|
92
|
+
deserializing data from disk. Otherwise memoizing will make it slower.
|
93
|
+
|
94
|
+
This depends on arguments having a unique serialization via the Marshal
|
95
|
+
library. Certain Ruby constructs cannot be serialized in this way and
|
96
|
+
may raise TypeErrors. See the
|
97
|
+
[Marshal documentation](http://www.ruby-doc.org/core-2.0/Marshal.html) for details.
|
98
|
+
|
99
|
+
|
100
|
+
## Constants
|
101
|
+
PersistentMemoize::PERSISTENT_MEMOIZE_VERSION
|
102
|
+
|
103
|
+
Returns the version of this package as a String.
|
104
|
+
|
105
|
+
## Methods
|
106
|
+
PersistentMemoize#memoize(method, path)
|
107
|
+
Takes a _method_ (symbol) and caches the results of _method_, for
|
108
|
+
particular arguments, on disk, in files under _path_.
|
109
|
+
|
110
|
+
If you call _method_ again with the same arguments, _memoize_ gives
|
111
|
+
you the value from disk instead of letting the method compute the
|
112
|
+
value again.
|
113
|
+
|
114
|
+
## Acknowledgements
|
115
|
+
|
116
|
+
Based on memoize by Daniel Berg (https://github.com/djberg96/memoize)
|
117
|
+
|
118
|
+
Daniel's library has a file-oriented cache but it creates a single cache file per
|
119
|
+
method, containing all the cached results. For my workloads, this cache file
|
120
|
+
gets very large and rewriting it is slow. So persistent_memoize stores each result
|
121
|
+
in its own file.
|
122
|
+
|
123
|
+
Daniel also included this note in his code, so I might as well acknowledge
|
124
|
+
these people too:
|
125
|
+
|
126
|
+
Code borrowed from Nobu Nakada (ruby-talk:155159).
|
127
|
+
Code borrowed from Ara Howard (ruby-talk:173428).
|
128
|
+
Code borrowed from Andrew Johnson (http://tinyurl.com/8ymx8)
|
129
|
+
Ideas taken from Brian Buckley and Sean O'Halpin.
|
130
|
+
Tiny URL provided for Andrew Johnson because I could not find the ruby-talk
|
131
|
+
reference. The gateway may have been broken at the time.
|
132
|
+
|
133
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
###################################################################
|
2
|
+
# fibonacci.rb
|
3
|
+
#
|
4
|
+
# Demonstrates, via Benchmark, the difference between a memoized
|
5
|
+
# version of the fibonnaci method versus a non-memoized version.
|
6
|
+
#
|
7
|
+
# You can run this via the 'example_fib' rake task.
|
8
|
+
###################################################################
|
9
|
+
require 'benchmark'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'persistent_memoize'
|
12
|
+
include PersistentMemoize
|
13
|
+
|
14
|
+
# Our fibonacci function
|
15
|
+
def fib(n)
|
16
|
+
return n if n < 2
|
17
|
+
fib(n-1) + fib(n-2)
|
18
|
+
end
|
19
|
+
|
20
|
+
file = File.join((ENV['HOME'] || ENV['USERPROFILE']), 'fib.cache')
|
21
|
+
|
22
|
+
max_iter = ARGV[0].to_i
|
23
|
+
max_fib = ARGV[1].to_i
|
24
|
+
|
25
|
+
max_iter = 100 if max_iter == 0
|
26
|
+
max_fib = 25 if max_fib == 0
|
27
|
+
|
28
|
+
print "\nBenchmarking against version: " + PERSISTENT_MEMOIZE_VERSION + "\n\n"
|
29
|
+
|
30
|
+
Benchmark.bm(35) do |x|
|
31
|
+
x.report("Not memoized:"){
|
32
|
+
max_iter.times{ fib(max_fib) }
|
33
|
+
}
|
34
|
+
|
35
|
+
x.report("Memoized to file:"){
|
36
|
+
memoize(:fib, file)
|
37
|
+
max_iter.times{ fib(max_fib) }
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
FileUtils.remove_dir(file) if File.exists?(file)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'fileutils'
|
3
|
+
require "persistent_memoize/version"
|
4
|
+
|
5
|
+
module PersistentMemoize
|
6
|
+
# Memoize the method 'name', with results stored in files under 'path'
|
7
|
+
def memoize(name, path)
|
8
|
+
unless File.exists?(path)
|
9
|
+
FileUtils.mkdir_p(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
(class<<self; self; end).send(:define_method, name) do |*args|
|
13
|
+
key = Digest::MD5.hexdigest(Marshal.dump(args))
|
14
|
+
cacheFile = File.join(path, key)
|
15
|
+
if File.exists?(cacheFile)
|
16
|
+
results = File.open(cacheFile, 'rb'){ |f| Marshal.load(f) }
|
17
|
+
else
|
18
|
+
results = super(*args)
|
19
|
+
File.open(cacheFile, 'wb'){ |f| Marshal.dump(results, f) }
|
20
|
+
end
|
21
|
+
results
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'persistent_memoize/version'
|
5
|
+
|
6
|
+
# Build the gem with the 'rake gem:build' command.
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'persistent_memoize'
|
10
|
+
spec.version = PersistentMemoize::VERSION
|
11
|
+
spec.version = '0.0.1'
|
12
|
+
spec.authors = ['Neil Kandalgaonkar']
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.email = ['neilk@brevity.org']
|
15
|
+
spec.homepage = 'https://github.com/neilk/persistent_memoize'
|
16
|
+
spec.platform = Gem::Platform::RUBY
|
17
|
+
spec.summary = 'Speeds up methods with caches, which persistent on disk'
|
18
|
+
spec.has_rdoc = true
|
19
|
+
spec.files = `git ls-files`.split($/)
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.extra_rdoc_files = ['README.md']
|
25
|
+
|
26
|
+
spec.add_development_dependency('test-unit', '>= 2.0.2')
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
|
30
|
+
spec.description = <<-EOF
|
31
|
+
This allows you to cache method calls for faster execution, at the cost of
|
32
|
+
storage space and IO operations. The caches are kept on disk,
|
33
|
+
persistent between executions of the program.
|
34
|
+
EOF
|
35
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
###############################################
|
2
|
+
# test_memoize.rb
|
3
|
+
#
|
4
|
+
# Test suite for the memoize library.
|
5
|
+
###############################################
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'test-unit'
|
8
|
+
|
9
|
+
require 'test/unit'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'persistent_memoize'
|
12
|
+
|
13
|
+
class TC_PersistentMemoize < Test::Unit::TestCase
|
14
|
+
include PersistentMemoize
|
15
|
+
|
16
|
+
def setup
|
17
|
+
@path = File.join((ENV['HOME'] || ENV['USERPROFILE']), 'test.cache')
|
18
|
+
end
|
19
|
+
|
20
|
+
def add(x, y)
|
21
|
+
return x + y
|
22
|
+
end
|
23
|
+
|
24
|
+
def fib(n)
|
25
|
+
return n if n < 2
|
26
|
+
fib(n-1) + fib(n-2)
|
27
|
+
end
|
28
|
+
|
29
|
+
def factorial(n)
|
30
|
+
f = 1
|
31
|
+
n.downto(2) { |x| f *= x }
|
32
|
+
f
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_memoize
|
36
|
+
assert_nothing_raised{ fib(5) }
|
37
|
+
assert_nothing_raised{ memoize(:fib, @path) }
|
38
|
+
assert_nothing_raised{ fib(50) }
|
39
|
+
assert_equal(55, fib(10))
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_memoize_directory_properties
|
43
|
+
assert(!File.exists?(@path), "path did not exist")
|
44
|
+
assert_nothing_raised{ memoize(:add, @path) }
|
45
|
+
assert(File.exists?(@path), "path does exist")
|
46
|
+
assert(Dir.entries(@path).select{ |f| f != '.' and f != '..' }.length == 0, "path is empty of files")
|
47
|
+
assert_nothing_raised{ add(10, 20) }
|
48
|
+
assert(Dir.entries(@path).select{ |f| f != '.' and f != '..' }.length == 1, "path has exactly one file")
|
49
|
+
end
|
50
|
+
|
51
|
+
def teardown
|
52
|
+
FileUtils.remove_dir(@path) if File.exists?(@path)
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: persistent_memoize
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Neil Kandalgaonkar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-01 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: test-unit
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.0.2
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.0.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.3'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.3'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: ! " This allows you to cache method calls for faster execution, at
|
63
|
+
the cost of \n storage space and IO operations. The caches are kept on disk,\n
|
64
|
+
\ persistent between executions of the program. \n"
|
65
|
+
email:
|
66
|
+
- neilk@brevity.org
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files:
|
70
|
+
- README.md
|
71
|
+
files:
|
72
|
+
- .gitignore
|
73
|
+
- Gemfile
|
74
|
+
- LICENSE.txt
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- examples/example_fibonacci.rb
|
78
|
+
- lib/persistent_memoize.rb
|
79
|
+
- lib/persistent_memoize/version.rb
|
80
|
+
- persistent_memoize.gemspec
|
81
|
+
- test/test_memoize.rb
|
82
|
+
homepage: https://github.com/neilk/persistent_memoize
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.8.23
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Speeds up methods with caches, which persistent on disk
|
107
|
+
test_files:
|
108
|
+
- test/test_memoize.rb
|