Upload
ztellman
View
193
Download
0
Tags:
Embed Size (px)
Citation preview
PREDICTABLY FAST CLOJURE
Zach Tellman @ztellman
(nth s 2)
(nth s 2)
(.nth ^clojure.lang.Indexed s 2)
(nth s 2)
(java.lang.reflect.Array/get s 2)
(.nth ^clojure.lang.Indexed s 2)
(nth s 2)
(java.lang.reflect.Array/get s 2)
(.get ^java.util.RandomAccess s 2)
(.nth ^clojure.lang.Indexed s 2)
(nth s 2)
(java.lang.reflect.Array/get s 2)
(.get ^java.util.RandomAccess s 2)
(.nth ^clojure.lang.Indexed s 2)
(Character. (.charAt ^CharSequence s 2))
(nth s 2)
(java.lang.reflect.Array/get s 2)
(.get ^java.util.RandomAccess s 2)
(.nth ^clojure.lang.Indexed s 2)
(Character. (.charAt ^CharSequence s 2))
(first (next (next s)))
REFERENTIAL TRANSPARENCY
(+ 1 1)
2
~
REFERENTIAL TRANSPARENCY
(f x)
((memoize f) x)~
REFERENTIAL TRANSPARENCY
(map f s)
(doall (map f s))
~
REFERENTIAL TRANSPARENCY
(map f s)
(pmap f s)~
(map f s)
(map (fn [x] (Thread/sleep 1000) (f x)) s)
?
REFERENTIAL TRANSPARENCY
ASSUMES INFINITE RESOURCES
REFERENTIAL TRANSPARENCY
IS CONTEXTUAL
THE UTILITY OF ANY ABSTRACTION
IS CONTEXTUAL
CORRECTNESS IS
CONTEXTUAL
PREDICTABLY FAST CLOJURE
Zach Tellman @ztellman
LOOKING DEEP INTO THE ABYSS
Zach Tellman @ztellman
LOOKING DEEP INTO THE ABYSS,
USING CLOJURE
Zach Tellman @ztellman
WHAT TO DO WHEN YOUR ASSUMPTIONS
COME CRASHING DOWN AROUND YOU
Zach Tellman @ztellman
MY RESPONSIBILITIES
• 100k requests/sec, at peak
• intake of terabytes of compressed data per day
• eight hours of uninterrupted sleep
MY RESPONSIBILITIES
• looking deep into the abyss, using Clojure
• waiting for my assumptions to come crashing down around me
(count s)
(defn count {:inline (fn [x] `(. clojure.lang.RT (count ~x)))} [coll] (clojure.lang.RT/count coll))
public static int count(Object o) { if(o instanceof Counted) return ((Counted) o).count(); return countFrom(Util.ret1(o, o = null)); }
(let [v [1 2 3]] (quick-bench (count v)))
(let [v [1 2 3]] (quick-bench (.count ^Counted v)))
~10 ns
~5 ns
(defn matching-index [offset ks prefix] (let [cnt-ks (- (count ks) offset) cnt-prefix (Array/getLength prefix) cnt (Math/min cnt-ks cnt-prefix)] (loop [idx 0] (if (== cnt idx) (+ offset idx) (if (= (nth ks (+ offset idx)) (aget prefix idx)) (recur (inc idx)) (+ offset idx))))))
~200 ns
(defn matching-index [offset ks prefix] (let [cnt-ks (- (.count ^Counted ks) offset) cnt-prefix (Array/getLength prefix) cnt (Math/min cnt-ks cnt-prefix)] (loop [idx 0] (if (== cnt idx) (+ offset idx) (if (= (nth ks (+ offset idx)) (aget prefix idx)) (recur (inc idx)) (+ offset idx))))))
~100 ns
PERFORMANCE IS
ALMOST NEVER THE
SUM OF ITS PARTS
WHY IS IT SLOWER THAN JAVA?
THE ETERNAL QUESTION:
(+ 2 1) !
(+ (Long. 2) 1) !
(+ (/ 3 2) (/ 3 2)) !
(+ 2 (BigInteger. 1))
NO.DISASSEMBLE
> (use 'no.disassemble) nil !
> (defn log [x] (Math/log x)) #’log !
> (println (disassemble log))
public final class user$log extends clojure.lang.AFunction { public static {}; ... public user$log(); ... public java.lang.Object invoke(java.lang.Object x); ... }
public java.lang.Object invoke(java.lang.Object x); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 checkcast java.lang.Number 6 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 9 invokestatic java.lang.Math.log(double) : double 12 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 15 areturn
(fn [x] (Math/log x))
public java.lang.Object invoke(java.lang.Object x); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 checkcast java.lang.Number 6 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 9 invokestatic java.lang.Math.log(double) : double 12 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 15 areturn
(fn [x] (Math/log x))
public java.lang.Object invoke(java.lang.Object x); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 checkcast java.lang.Number 6 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 9 invokestatic java.lang.Math.log(double) : double 12 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 15 areturn
(fn [x] (Math/log x))
public java.lang.Object invoke(java.lang.Object x); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 checkcast java.lang.Number 6 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 9 invokestatic java.lang.Math.log(double) : double 12 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 15 areturn
(fn [x] (Math/log x))
public java.lang.Object invoke(java.lang.Object x); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 checkcast java.lang.Number 6 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 9 invokestatic java.lang.Math.log(double) : double 12 invokestatic java.lang.Double.valueOf(double) : java.lang.Double 15 areturn
(fn [x] (Math/log x))
(fn ^double [^double x] (Math/log x))
public java.lang.Object invoke(java.lang.Object arg0); 0 aload_0 1 aload_1 2 checkcast java.lang.Number 5 invokestatic clojure.lang.RT.doubleCast(java.lang.Object) : double 8 invokeinterface clojure.lang.IFn$DD.invokePrim(double) : double 13 new java.lang.Double 16 dup_x2 17 dup_x2 18 pop 19 invokespecial java.lang.Double(double) 22 areturn
(fn ^double [^double x] (Math/log x))
public final double invokePrim(double x); 0 dload_1 [x] 1 invokestatic java.lang.Math.log(double) : double 4 dreturn
(fn [x y] (Math/min x y))
public java.lang.Object invoke(java.lang.Object x, java.lang.Object y); 0 ldc <String "java.lang.Math"> 2 invokestatic java.lang.Class.forName(java.lang.String) : java.lang.Class 5 ldc <String "min"> 7 iconst_2 8 anewarray java.lang.Object 11 dup 12 iconst_0 13 aload_1 [x] 14 aconst_null 15 astore_1 [x] 16 aastore 17 dup 18 iconst_1 19 aload_2 [y] 20 aconst_null 21 astore_2 22 aastore 23 invokestatic clojure.lang.Reflector.invokeStaticMethod … 26 areturn
(fn [x y] (+ x y))
public java.lang.Object invoke(java.lang.Object x, java.lang.Object y); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 aload_2 [y] 4 aconst_null 5 astore_2 [y] 6 invokestatic clojure.lang.Numbers.add(java.lang.Object, java.lang.Object) … 9 areturn
static public Number add(Object x, Object y){ return ops(x).combine(ops(y)).add((Number)x, (Number)y); } class LongOps { final public Number add(Number x, Number y){ return num(Numbers.add(x.longValue(),y.longValue())); } } static public long add(long x, long y){ long ret = x + y; if ((ret ^ x) < 0 && (ret ^ y) < 0) return throwIntOverflow(); return ret; }
(fn [x y] (+ x y))
(fn [^long x ^long y] (+ x y))
public final java.lang.Object invokePrim(long x, long arg1); 0 lload_1 [x] 1 lload_3 2 invokestatic clojure.lang.Numbers.add(long, long) : long 5 invokestatic clojure.lang.Numbers.num(long) : java.lang.Number 8 areturn
(fn ^long [^long x ^long x] (+ x y))
public final long invokePrim(long x, long arg1); 0 lload_1 [x] 1 lload_3 2 invokestatic clojure.lang.Numbers.add(long, long) : long 5 lreturn
(fn ^long [^long x ^long y] (unchecked-add x y))
public final long invokePrim(long x, long arg1); 0 lload_1 [x] 1 lload_3 2 ladd 5 lreturn
(fn ^long [^long x ^long y] (unchecked-add x y))
AS FAST AS JAVA!
public final long invokePrim(long x, long arg1); 0 lload_1 [x] 1 lload_3 2 ladd 5 lreturn
(fn [x y] (unchecked-add x y))
public java.lang.Object invoke(java.lang.Object x, java.lang.Object y); 0 aload_1 [x] 1 aconst_null 2 astore_1 [x] 3 aload_2 [y] 4 aconst_null 5 astore_2 [y] 6 invokestatic clojure.lang.Numbers.unchecked_add(java.lang.Object, … 9 areturn
AND SO…
public class Primitives { ! … ! public static long add(long a, long b) { return a + b; } public static double add(double a, double b) { return a + b; } ! … !}
> (require '[primitive-math :as p]) nil !
> (macroexpand '(p/+ x y)) (. primitive_math.Primitives add x y)
(fn ^long [^long x ^long y] (p/+ x y))
public final long invokePrim(long x, long arg1); 0 lload_1 [x] 1 lload_3 2 invokestatic primitive_math.Primitives.add(long, long) : long 5 lreturn
> (set! *warn-on-reflection* true) true !> (fn [x y] (unchecked-add x y)) #<...> !> (fn [x y] (p/+ x y)) Reflection warning - call to add can't be resolved. #<...>
• does not supplant Clojure’s numerics
• invariants via feedback when they aren’t satisfied
• using Java isn’t cheating
IN THE BEGINNING, THERE WAS THE INPUT STREAM
AND SO…
(quick-bench (.getBytes "a reasonably long string"))
(let [f (memoize identity)] (quick-bench (f 1)))
~60 ns
~100 ns
(defn memoize [f] (let [mem (atom {})] (fn [& args] (if-let [e (find @mem args)] (val e) (let [ret (apply f args)] (swap! mem assoc args ret) ret)))))
{} or (array-map)
(hash-map)
• up to eight calls to .equiv()
A LOOKUP
• one call to .hasheq(), approx. one call to .equiv()
if(obj instanceof IPersistentVector) { Collection ma = (Collection) obj; if (ma.size() != v.count()) return false; for(Iterator i1 = ((List) v).iterator(), i2 = ma.iterator(); i1.hasNext();) { if (!Util.equiv(i1.next(), i2.next())) return false; } return true; }
AND SO…
(deftype Tuple0 []) !
(deftype Tuple1 [a]) !
(deftype Tuple2 [a b]) !
(deftype Tuple3 [a b c]) !
…
(if (instance? ~name x##) ~(if (zero? cardinality) true `(and ~@(map (fn [f] `(Util/equiv ~f (. ~other ~f))) fields))) …)
(let [v [1 2 3]] (quick-bench (nth v 0)))
(let [v [1 2 3]] (quick-bench (first v)))
~5 ns
~50 ns
(let [t (tuple 1 2 3)] (quick-bench (nth t 0)))
(let [t (tuple 1 2 3)] (quick-bench (first t)))
~5 ns
~5 ns
• if you can, do the hard work once
• if you don’t control the context, assume every bit matters
• if you can, do the hard work at compile-time
• the tools for library-sized macros are a bit lacking right now
I WORK WITH THIS GUY
(he is programming)
AND SO…
MUTABILITY!
(let-mutable [x 0] (dotimes [_ 100] (set! x (inc x))) x)
(let [x (LongContainer. 0)] (dotimes [_ 100] (.set x (inc (.get x)))) (.get x))
(let-mutable [x 1] (fn [] x))
(let [x (LongContainer. 1)] (let [x (.get x)] (fn [] x)))
• Clojure’s general solutions are fairly pessimistic, and assume few invariants
• try creating a more optimistic context
YOU DON’T HAVE TO DO WHAT HE DID
AND SO…
SOME FINAL THOUGHTS
• be curious about the tools you use
• if you’re going for a bounded solution, describe the boundaries fully
• if you’re going for a general solution, make sure it’s actually general
QUESTIONS?