A Tachy[1] 'TAK'
Henry G. Baker
Nimble Computer Corporation, 16231 Meadow Ridge Way, Encino, CA 91436
(818) 501-4956 (818) 986-1360 (FAX)
Copyright (c) 1992 by Nimble Computer Corporation
We show how to speed up the Tak Benchmark by an order of magnitude--5X faster
than the Cray-1--on a Common Lisp system (40MHz 80860-based OKIstation) using
memoizing. The list-based Takl Benchmark improves even more--30X faster than
the Cray-1. Given the speed attainable through memoizing, the possibility of
further speedups using parallelism seems unlikely.
A. INTRODUCTION
The Tak benchmark is John McCarthy's mis-remembered version of the Takeuchi
function [Gabriel85]. The Tak benchmark is one of the most commonly used
benchmarks because its reliance on only recursive function-calling and integer
arithmetic allows it to be used early in hardware debugging, and because it is
short enough to memorize and type surreptitiously into a competitor's computer
at a trade show. While some benchmarks have been criticized for running
"entirely within the cache", the Tak benchmark typically runs "entirely within
the register set" of a RISC architecture, and therefore deserves a double dose
of the same criticism. It is generally assumed that because of its ubiquity
that Tak cannot be speeded up by non-intelligent means; we show that this
assumption is erroneous.
We show that Tak can be speeded up by the technique of "memoization" [Bird80]
[Keller86], which requires only that the function be "functional"--i.e.,
contain no side-effects. Since the lack of side-effects can often be
statically assured at compile time by simple syntactic tests, a compiler could
decide to utilize memoization for Tak as one of its standard optimizations.
B. STANDARD 'TAK'
According to [Gabriel85], the Tak benchmark contains 63,609 recursive calls to
tak, as well as 47,706 decrement operations, when performed on the
arguments (18 12 6) to produce the answer 7. None of the
arguments to tak ever becomes negative, nor does any ever exceed
18. The first arm of the conditional is executed 75% of the time.
(defun tak (x y z)
(if (not (< y x)) z
(tak (tak (1- x) y z)
(tak (1- y) z x)
(tak (1- z) x y))))
C. MEMOIZING 'TAK'
A simple measurement shows that tak is called with only 281 distinct
combinations of arguments, so memoization can work splendidly. However, in
order to memoize, we must construct a single "key" from the triple of integers
passed to tak as arguments. The Lispiest way to do this is to
construct a Lisp list of the 3 arguments, and then use this as a key to a
Common Lisp equal hash table, as in the following code:
(defparameter *memo-table* (make-hash-table :test #'equal)
"Those who don't remember the past are condemned to recompute it"[2])
(defun make-key (x y z) `(,x ,y ,z))
(defun tak (x y z)
(let ((key (make-key x y z))) ; [3]
(or (gethash key *memo-table*)
(setf (gethash key *memo-table*)
(if (not (< y x)) z
(tak (tak (1- x) y z)
(tak (1- y) z x)
(tak (1- z) x y)))))))
This implementation works, and can already out-perform many standard
tak implementations. It can be speeded up by the straight-forward
technique of "hash consing"
[Ershov58]
[Goto74] [Deutsch73], which allows the
equal hash table to be replaced by an eq hash table. But the
fastest implementation utilizes the fact that the argument integers are
bounded, and we can therefore pack them into a single fixnum:
(defparameter *memo-table* (make-hash-table :test #'eq))
(defun make-key (x y z) (+ (ash x 16) (ash y 8) z))
D. STANDARD 'TAKL'
The Gabriel Takl benchmark is obtained from the Tak benchmark by replacing
integer counters with list counters; i.e., lists of length n are used to
represent the integer n. Intuitively, one would presume that Takl would
run a small factor slower than Tak, since list counters would appear to be only
a small factor slower than fixnum counters (assuming that the lists are in the
cache). However, it is much more difficult to implement the <
predicate on lists than on fixnums; therefore, shorterp takes time
proportional to the smaller of its arguments instead of taking only a small
constant amount of time. On the standard benchmark versions, we find
non-memoized Takl to be about 5.7X slower than non-memoized Tak.
E. MEMOIZING 'TAKL'
Memoizing Takl is slightly more difficult than memoizing Tak, because we cannot
utilize packed integers as the keys to our memo table, but must construct
unique keys using hash consing. However, our table still consists of only 281
active entries, so it will likely remain entirely within the cache.
In Takl, we actually have a choice about whether to memoize mas,
shorterp or both. While memoizing shorterp should
dramatically shorten its time, we would still execute the entire 63,609 number
of calls to mas. If we memoize mas, then we are left with
very few calls to shorterp, in which case its timing won't matter very
much. Thus, it is only necessary to memoize mas to get most of the
benefits of memoization.
F. RESULTS
The memoization optimization improves Tak by about an order of magnitude. We
achieve a Tak time of 0.008 seconds on the 40Mhz 80860-based OKIstation(TM),
which time is 5 X faster than the Cray-1 on the old benchmark.[4] By utilizing memoization with hash consing on
the Takl benchmark, we achieve a Takl time of 0.01 seconds, which is 30 X
faster than the Cray-1 on the old benchmark. Interestingly enough, Takl is
only 25% slower than Tak when both are memoized; these numbers indicate that
the memo table lookup dominates both computations.
G. REFERENCES
Anderson, J.Wayne, et al. "Implementing and Optimizing Lisp for the
Cray". IEEE Software (July 1987),74-83.
Bird, R.S. "Tabulation Techniques for Recursive Programs". ACM Comp. Surv.
12,4 (Dec. 1980),403-417.
Deutsch, L. Peter. "An Interactive Program Verifier". Xerox PARC TR CSL-73-1,
1973.
[Ershov58]
Ershov, A.P. "On Programming of Arithmetic Operations". Doklady, AN USSR
118,3 (1958),427-430, transl. Friedman, M.D., CACM 1,8 (Aug.
1958),3-6.
Gabriel, R.P. Performance and Evaluation of Lisp Systems. MIT Press,
Camb., MA, 1985.
Goto, Eiichi. "Monocopy and Associative Algorithms in Extended Lisp". TR.
74-03, U. Tokyo, 1974.
Keller, R.M., and Sleep, M.R. "Applicative Caching". ACM TOPLAS 8,1
(Jan. 1986),88-108.
[Steele90]
Steele, Guy L. Common Lisp, The Language; 2nd Ed. Digital Press,
Bedford, MA, 1990,1029p.
[1]
Look it up in your Funk&Wagnall's.
[2]
Apologies to Santayana.
[3]
One might also utilize &rest arguments to
construct the key list, as in (defun tak (&rest key) ... ).
[4]
We are using the "old" Cray-1 numbers from [Gabriel85];
newer numbers for the Cray-1 are given in [Anderson87].