Usage examples

Usage examples

The following examples show how Caching can be employed to cache function outputs. Most examples employ the macros as this is the most straightforward usage pattern.

The Cache object

The caching object is named Cache and it can be easily constructed using the @cache macro. There are several supported expressions that can be used to construct Caches:

julia> using Caching, InteractiveUtils, Serialization
julia> @cache function foo(x)
           x+1
       end
foo (cache with 0 entries, 0 in memory 0 on disk)

julia> typeof(foo)
Cache{typeof(Main.ex-index.f_CDLnhxHDfxcK1ZtyhuhY),Any,CountSize}

julia> @code_warntype foo(1)
Variables
  cache::Cache{typeof(Main.ex-index.f_CDLnhxHDfxcK1ZtyhuhY),Any,CountSize}
  args::Tuple{Int64}

Body::ANY
1 ─ %1 = Core.NamedTuple()::Core.Compiler.Const(NamedTuple(), false)
│   %2 = Base.pairs(%1)::Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false)
│   %3 = Core.tuple(%2, cache)::Core.Compiler.PartialStruct(Tuple{Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}},Cache{typeof(Main.ex-index.f_CDLnhxHDfxcK1ZtyhuhY),Any,CountSize}}, Any[Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false), Cache{typeof(Main.ex-index.f_CDLnhxHDfxcK1ZtyhuhY),Any,CountSize}])
│   %4 = Core._apply(Caching.:(var"#_#4"), %3, args)::ANY
└──      return %4

or, for type stability,

julia> @cache function foo2(x)::Int
           x+1
       end
foo2 (cache with 0 entries, 0 in memory 0 on disk)

julia> @code_warntype foo2(1)
Variables
  cache::Cache{typeof(Main.ex-index.f_bfHJuGeubaFR9B4r2j0t),Int64,CountSize}
  args::Tuple{Int64}

Body::Int64
1 ─ %1 = Core.NamedTuple()::Core.Compiler.Const(NamedTuple(), false)
│   %2 = Base.pairs(%1)::Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false)
│   %3 = Core.tuple(%2, cache)::Core.Compiler.PartialStruct(Tuple{Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}},Cache{typeof(Main.ex-index.f_bfHJuGeubaFR9B4r2j0t),Int64,CountSize}}, Any[Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false), Cache{typeof(Main.ex-index.f_bfHJuGeubaFR9B4r2j0t),Int64,CountSize}])
│   %4 = Core._apply(Caching.:(var"#_#4"), %3, args)::Int64
└──      return %4

The approach works for anonymous functions as well:

julia> @cache foo3 = x->x-1
foo3 (cache with 0 entries, 0 in memory 0 on disk)

or, for type stability,

julia> @cache foo4 = x::Int->x-1
foo4 (cache with 0 entries, 0 in memory 0 on disk)

julia> @code_warntype foo3(1)
Variables
  cache::Cache{Main.ex-index.var"#1#2",Any,CountSize}
  args::Tuple{Int64}

Body::ANY
1 ─ %1 = Core.NamedTuple()::Core.Compiler.Const(NamedTuple(), false)
│   %2 = Base.pairs(%1)::Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false)
│   %3 = Core.tuple(%2, cache)::Core.Compiler.PartialStruct(Tuple{Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}},Cache{Main.ex-index.var"#1#2",Any,CountSize}}, Any[Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false), Cache{Main.ex-index.var"#1#2",Any,CountSize}])
│   %4 = Core._apply(Caching.:(var"#_#4"), %3, args)::ANY
└──      return %4

julia> @code_warntype foo4(1)
Variables
  cache::Cache{Main.ex-index.var"#3#4",Int64,CountSize}
  args::Tuple{Int64}

Body::Int64
1 ─ %1 = Core.NamedTuple()::Core.Compiler.Const(NamedTuple(), false)
│   %2 = Base.pairs(%1)::Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false)
│   %3 = Core.tuple(%2, cache)::Core.Compiler.PartialStruct(Tuple{Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}},Cache{Main.ex-index.var"#3#4",Int64,CountSize}}, Any[Core.Compiler.Const(Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}(), false), Cache{Main.ex-index.var"#3#4",Int64,CountSize}])
│   %4 = Core._apply(Caching.:(var"#_#4"), %3, args)::Int64
└──      return %4

Memory and disk memoization

The Cache object itself supports reading/writing cached entries from/to memory and to disk.

Note

Re-using the cached function outputs from a file is not possible once the in-memory Cache object goes out of scope.

julia> foo5(x) = x+1
foo5 (generic function with 1 method)

julia> dc = @cache foo5 "somefile.bin"
foo5 (cache with 0 entries, 0 in memory 0 on disk)

julia> dc(1);  # add one entry to cache

julia> dc.cache
Dict{UInt64,Any} with 1 entry:
  0xc159d77b031aa8af => 2

julia> dc.offsets  # disk cache information (hash=>(start byte, end byte))
Dict{UInt64,Tuple{UInt64,UInt64}} with 0 entries

julia> dc.filename  # file information
"/home/travis/build/zgornel/Caching.jl/docs/build/somefile.bin"

julia> isfile(dc.filename)  # file does not exist
false

The cache can be written to disk using the persist! function or the @persist! macro:

julia> @persist! dc  # writes cache to disk and updates offsets
foo5 (cache with 1 entry, 1 in memory 1 on disk)

julia> isfile(dc.filename)
true

julia> dc.offsets
Dict{UInt64,Tuple{UInt64,UInt64}} with 1 entry:
  0xc159d77b031aa8af => (0x0000000000000009, 0x0000000000000012)

The cache can be deleted using the empty! function or the @empty! macro:

julia> @empty! dc  # delete memory cache
foo5 (cache with 1 entry, 0 in memory 1 on disk)

julia> @empty! dc true  # delete also the disk cache
foo5 (cache with 0 entries, 0 in memory 0 on disk)

julia> isfile("somefile.bin")
false

If no file name is provided when creating a Cache object, a file name will be automatically generated:

julia> dc = @cache foo5
foo5 (cache with 0 entries, 0 in memory 0 on disk)

julia> dc.filename
"/home/travis/build/zgornel/Caching.jl/docs/build/_9aec8ed5aa424656_.bin"

Cache misses

In case of a cache memory miss, the cached data is retrieved from disk if available:

julia> dc = @cache foo5::Int "somefile.bin"
foo5 (cache with 0 entries, 0 in memory 0 on disk)

julia> for i in 1:3 dc(i); end              # add 3 entries

julia> @persist! dc
foo5 (cache with 3 entries, 3 in memory 3 on disk)

julia> @assert isfile("somefile.bin")

julia> @empty! dc                           # empty memory cache
foo5 (cache with 3 entries, 0 in memory 3 on disk)

julia> @assert isempty(dc.cache)

julia> for i in 4:6 dc(i); end              # add 3 new entries

julia> dc
foo5 (cache with 6 entries, 3 in memory 3 on disk)

julia> dc(1)  # only on disk
2

julia> dc(4)  # in memory
5

Memory-disk synchronization

Synchronization between the memory and disk cache contents is done with the help of the syncache! function and @syncache! macro:

julia> dc = @cache foo5 "somefile.bin"       # make a Cache object
foo5 (cache with 0 entries, 0 in memory 0 on disk)

julia> for i in 1:5 dc(i); end              # populate the memory cache with 5 entries

julia> @persist! dc                         # write to disk the cache the 5 entries
foo5 (cache with 5 entries, 5 in memory 5 on disk)

julia> @empty! dc                           # delete the memory cache
foo5 (cache with 5 entries, 0 in memory 5 on disk)

julia> @syncache! dc "disk"                 # load cache from disk
foo5 (cache with 5 entries, 5 in memory 5 on disk)

julia> @empty! dc  # empty memory cache
foo5 (cache with 5 entries, 0 in memory 5 on disk)

julia> for i in 1:3  dc(-i); end            # populate the memory cache with 3 new entries

julia> @syncache! dc "memory"               # write memory cache to disk
foo5 (cache with 8 entries, 3 in memory 8 on disk)

julia> @empty! dc
foo5 (cache with 8 entries, 0 in memory 8 on disk)

julia> @syncache! dc "disk"                 # load cache from disk
foo5 (cache with 8 entries, 8 in memory 8 on disk)

julia> dc.cache  # view the cache
Dict{UInt64,Any} with 8 entries:
  0xc0e8089aaf6365bf => 6
  0xcfd1bb447236eae7 => 4
  0x5717fd56af052f04 => 0
  0xc159d77b031aa8af => 2
  0x36317c6768130f8c => 3
  0x1ac8353e20a225f8 => -1
  0xf0f39ef33944221b => 5
  0xa0c5b6bee44db0f8 => -2

julia> dc.offsets  # view the file offsets
Dict{UInt64,Tuple{UInt64,UInt64}} with 8 entries:
  0xc0e8089aaf6365bf => (0x000000000000002d, 0x0000000000000036)
  0xc159d77b031aa8af => (0x0000000000000009, 0x0000000000000012)
  0xcfd1bb447236eae7 => (0x000000000000001b, 0x0000000000000024)
  0x5717fd56af052f04 => (0x0000000000000036, 0x000000000000003f)
  0x36317c6768130f8c => (0x0000000000000012, 0x000000000000001b)
  0x1ac8353e20a225f8 => (0x000000000000003f, 0x000000000000004c)
  0xf0f39ef33944221b => (0x0000000000000024, 0x000000000000002d)
  0xa0c5b6bee44db0f8 => (0x000000000000004c, 0x0000000000000059)

Synchronization of disk and memory cache contents can also be performed in one go by passing "both" in the @syncache! macro call:

julia> dc = @cache foo5;

julia> for i in 1:3 dc(i); end              # populate the memory cache with 3 entries

julia> @syncache! dc "memory"               # write to disk the 3 entries
foo5 (cache with 3 entries, 3 in memory 3 on disk)

julia> @empty! dc                           # delete the in-memory cache
foo5 (cache with 3 entries, 0 in memory 3 on disk)

julia> for i in 1:5 dc(-i); end             # populate the in-memory cache with 5 new entries

julia> @syncache! dc "both"                 # sync both memory and disk
foo5 (cache with 8 entries, 8 in memory 8 on disk)

julia> dc.cache
Dict{UInt64,Any} with 8 entries:
  0xcfd1bb447236eae7 => 4
  0x5717fd56af052f04 => 0
  0x54ccc0bf21828ced => -3
  0xc49d73501c07ec93 => -4
  0x1ac8353e20a225f8 => -1
  0x36317c6768130f8c => 3
  0xc159d77b031aa8af => 2
  0xa0c5b6bee44db0f8 => -2

Maximum sizes

Cache objects support maximum sizes in terms of either number of entries (i.e. function outputs) or the maximum memory size allowed:

julia> foo6(x) = x
foo6 (generic function with 1 method)

julia> dc = @cache foo6 "somefile.bin" 3     # 3 objects max; use Int for objects
foo6 (cache with 0 entries, 0 in memory 0 on disk)

julia> for i in 1:3 dc(i) end               # cache is full

julia> dc(4)                                # 1 is removed (FIFO rule)
4

julia> @assert !(1 in values(dc.cache)) &&
           all(i in values(dc.cache) for i in 2:4)

julia> @persist! dc
foo6 (cache with 3 entries, 3 in memory 3 on disk)

julia> @empty! dc                           # 2,3,4 on disk
foo6 (cache with 3 entries, 0 in memory 3 on disk)

julia> for i in 5:6 dc(i) end               # 5 and 6 in memory

julia> @syncache! dc                        # brings 4 (most recent on disk) in memory and writes 5,6 on disk
┌ Warning: Memory cache full, loaded 1 out of 3 entries.
└ @ Caching ~/build/zgornel/Caching.jl/src/utils.jl:129
foo6 (cache with 5 entries, 3 in memory 5 on disk)
julia> dc = @cache foo6 "somefile.bin" 1.0   # 1.0 --> 1 KiB = 1024 bytes max; use Float64 for KiB
foo6 (cache with 0 entries, 0 in memory 0 on disk)

julia> for i in 1:128 dc(i) end             # cache is full (128 x 8bytes/Int = 1024 bytes)

julia> dc(129)                              # 1 is removed
129

julia> @assert !(1 in values(dc.cache)) &&
           all(i in values(dc.cache) for i in 2:129)

julia> @persist! dc
foo6 (cache with 128 entries, 128 in memory 128 on disk)

julia> @empty! dc                           # 2,...,129 on disk, nothing in memory
foo6 (cache with 128 entries, 0 in memory 128 on disk)

julia> for i in 130:130+126 dc(i) end       # write 127 entries

julia> #--> 130,..,256 in memory, 2,...,129 on disk
       @syncache! dc                        # brings 129 in memory and 130,...,256 on disk
┌ Warning: Memory cache full, loaded 1 out of 128 entries.
└ @ Caching ~/build/zgornel/Caching.jl/src/utils.jl:129
foo6 (cache with 255 entries, 128 in memory 255 on disk)

Serialization

Caching 0.2.0

This feature requires version 0.2.0

It is possible to save and load Cache objects to and from disk. This does not refer to the disk cache associated with an object but rather the object itself. The straightforward approach is to generate a cache object through the @cache macro and define the function withing the scope of the call:

julia> @cache foo = x->begin println("this is foo."); true; end
foo (cache with 0 entries, 0 in memory 0 on disk)

julia> serialize("foo.serialized.bin", foo)

julia> foo_d = deserialize("foo.serialized.bin", Cache)
foo (cache with 0 entries, 0 in memory 0 on disk)

julia> @assert foo_d(1)  # result not cached
this is foo.

julia> foo_d(1)  # result cached
true

One can check the that the code of the function is captured:

julia> println(foo_d.func_def)
x::Any->begin
        #= /home/travis/build/zgornel/Caching.jl/src/cache.jl:201 =#
        begin
            #= none:1 =#
            #= none:1 =#
            println("this is foo.")
            #= none:1 =#
            true
        end
    end

The approach works in a similar way for definitions of the form @cache function bar(x) end

If the Cache object is created by directly specifying an existing function, the only way to recover full functionality is to manually specify the same function when deserializing:

julia> bar(x) = x
bar (generic function with 1 method)

julia> barc = @cache bar  # `bar` code is unknwon
bar (cache with 0 entries, 0 in memory 0 on disk)

julia> serialize("bar.serialized.bin", barc)

julia> bar_d = deserialize("bar.serialized.bin", Cache)  # fails, cannot recreate function `bar`
ERROR: Cannot reconstruct cache; use the `func` keyword argument.

julia> bar_d = deserialize("bar.serialized.bin", Cache; func=bar)  # works, bar is known
bar (cache with 0 entries, 0 in memory 0 on disk)

julia> bar_d(1)
1
Warning
  • Any function can be provided through the func keyword argument and this may result in undefined behavior; it is up to the user to provide the original cached function.

  • This approach is independent of the disk cache associated with the Cache object and portability has again to be explicitly ensured i.e. if moving the serialized objects and disk caches across machines the path of the disk cache (filename property) may have to be manually changed.

More usage examples can be found in the test/runtests.jl file.