1 /*
2     This file is part of BioD.
3     Copyright (C) 2012    Artem Tarasov <lomereiter@gmail.com>
4 
5     Permission is hereby granted, free of charge, to any person obtaining a
6     copy of this software and associated documentation files (the "Software"),
7     to deal in the Software without restriction, including without limitation
8     the rights to use, copy, modify, merge, publish, distribute, sublicense,
9     and/or sell copies of the Software, and to permit persons to whom the
10     Software is furnished to do so, subject to the following conditions:
11     
12     The above copyright notice and this permission notice shall be included in
13     all copies or substantial portions of the Software.
14     
15     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21     DEALINGS IN THE SOFTWARE.
22 
23 */
24 module bio.core.utils.memoize;
25 
26 import std.traits;
27 import std.typecons;
28 
29 debug {
30     import std.stdio;
31 }
32 
33 ///
34 interface Cache(K, V) {
35     ///
36     V* lookup(K key);
37 
38     ///
39     void put(K key, V value);
40 }
41 
42 /// Elements are not removed from the cache.
43 class BasicCache(uint maxItems, K, V) : Cache!(K, V) {
44     private V[K] cache; 
45 
46     V* lookup(K key) {
47         return key in cache;
48     }
49 
50     void put(K key, V value) {
51         cache[key] = value;
52     }
53 }
54 
55 /// First-in first-out element removal.
56 class FifoCache(uint maxItems, K, V) : Cache!(K, V) {
57     private V[K] cache; 
58     private int items = 0;
59     bool full = false;
60     private K[maxItems] keys; // cyclic queue
61     private uint removals;
62 
63     V* lookup(K key) {
64         return key in cache;
65     }
66 
67     void put(K key, V value) {
68         cache[key] = value;
69 
70         if (full) {
71             cache.remove(keys[items]);
72             removals += 1;
73 
74             if (removals % maxItems == 0) {
75                 cache.rehash;
76             }
77         }
78 
79         keys[items] = key;
80         items += 1;
81 
82         if (items == maxItems) {
83             full = true;
84             items = 0;
85         }
86     }
87 }
88 
89 /// Least used strategy
90 class LuCache(uint maxItems, K, V) : Cache!(K, V) {
91     private {
92         V[K] cache;
93         int[K] counter;
94         uint removals;
95     }
96 
97     V* lookup(K key) {
98         auto result = key in cache;
99         if (result !is null) {
100             counter[key] += 1;
101         }
102         return result;
103     }
104 
105     void put(K key, V value) {
106         if (counter.length >= maxItems) {
107             // delete one element before inserting next
108             int min = int.max;
109             K min_key;
110             foreach (k, i; counter) {
111                 if (i < min) {
112                     min = i;
113                     min_key = k;
114                 }
115             }
116             
117             cache.remove(min_key);
118             counter.remove(min_key);
119             removals += 1;
120 
121             if (removals % maxItems == 0) {
122                 cache.rehash;
123                 counter.rehash;
124             }
125         }
126         cache[key] = value;
127         counter[key] = 1;
128     }
129 }
130 
131 version(unittest) {
132     /// keeps number of function evaluations
133     static shared evaluations = 0;
134     static shared int hits = 0;
135     static shared int misses = 0;
136 
137     import std.stdio;
138     import core.atomic;
139     void printStats() {
140         writeln("hits: ", hits, " misses: ", misses);
141     }
142 }
143 
144 auto memoize(alias func, uint maxItems=1024,
145              alias CacheImpl=BasicCache, Args...)(Args args)
146     if(isCallable!func && is(Args == ParameterTypeTuple!func)
147                        && Args.length > 0) 
148 {
149     alias ReturnType!func R; 
150 
151     static if (Args.length == 1) {
152         static shared(CacheImpl!(maxItems, Args, R)) cache;
153     } else {
154         static shared(CacheImpl!(maxItems, Tuple!Args, R)) cache;
155     }
156     static shared bool init = false;
157 
158     if (!init) {
159         synchronized {
160             if (cache is null) {
161                 static if (Args.length == 1) {
162                     cache = new shared(CacheImpl!(maxItems, Args, R))();
163                 } else {
164                     cache = new shared(CacheImpl!(maxItems, Tuple!Args, R))();
165                 }
166             }
167             init = true;
168         }
169     }
170 
171     static if (Args.length == 1) {
172         auto key = args;
173     } else {
174         auto key = tuple(args);
175     }
176 
177     R* ret = (cast()cache).lookup(key);
178     if (ret !is null) {
179         version(unittest) {
180             atomicOp!"+="(hits, 1);
181         }
182         return *ret;
183     } else {
184         version(unittest) {
185             atomicOp!"+="(misses, 1);
186         }
187         synchronized(cache) {
188 
189             ret = (cast()cache).lookup(key);
190             if (ret !is null) {
191                 return *ret;
192             }
193 
194             version(unittest) {
195                 evaluations.atomicOp!"+="(1);
196             }
197 
198             auto result = func(args);
199             (cast()cache).put(key, result);
200             return result;
201         }
202     }
203 }
204 
205 unittest {
206 
207     import core.thread;
208 
209     evaluations = 0;
210     /// very simple function for testing
211     int func(int x, int y) {
212         return x * y;
213     }
214 
215     /// 4 different argument values in total
216     void thread_func() {
217         memoize!func(5, 10);
218         memoize!func(3, 7);
219         memoize!func(6, 4);
220         memoize!func(5, 10);
221         memoize!func(7, 9);
222     }
223     
224     Thread[] threads = new Thread[5];
225     foreach (i; 0 .. 5) threads[i] = new Thread(&thread_func);
226     foreach (i; 0 .. 5) threads[i].start();
227     foreach (i; 0 .. 5) threads[i].join();
228 
229     /// number of evaluations must be the same as number of 
230     /// different argument values (4 in this case)
231     assert(evaluations == 4);
232 
233     /// test FIFO cache
234     alias memoize!(func, 2, FifoCache, int, int) fifomemoize;
235     evaluations = 0;
236 
237     fifomemoize(1, 5); // 5
238     fifomemoize(2, 3); // 5 6
239     fifomemoize(2, 4); // 6 8
240     fifomemoize(1, 5); // 8 5
241     assert(evaluations == 4);
242     fifomemoize(2, 4); // 8 5
243     assert(evaluations == 4);
244     fifomemoize(1, 7); // 5 7
245     fifomemoize(1, 5); // 5 7
246     assert(evaluations == 5);
247 
248     int foo(int x) {
249         return x;
250     }
251     /// Test LU cache
252     alias memoize!(foo, 3, LuCache, int) lumemoize;
253     evaluations = 0;
254 
255     lumemoize(1);
256     lumemoize(1);
257     lumemoize(1); // 1
258     lumemoize(2);
259     lumemoize(2); // 1, 2
260     lumemoize(3);
261     lumemoize(3);
262     lumemoize(3); // 1, 2, 3
263     lumemoize(4); // 2 -> 4
264     lumemoize(2); // 4 -> 2
265     assert(evaluations == 5);
266     lumemoize(3); // 1, 2, 3
267     lumemoize(5); // 2 -> 5
268     assert(evaluations == 6);
269     lumemoize(4); // 5 -> 4
270     lumemoize(9); // 4 -> 9
271     assert(evaluations == 8);
272 }