1. Dynamic Dispatch vs. Static Dispatch

여기서 Dynamic의 의미는 '동적’이라는 의미로 run-time을 말하고, Static은 '정적’이라는 의미로 compile-time을 말한다. Dispatch는 어떤 함수를 호출할지를 결정하는 것을 의미한다. 따라서 Dymamic Dispatch는 run-time에 어떤 함수를 호출할지를 결정하는 것이고, Static Dispatch는 compile-time에 어떤 함수를 호출할지를 결정하는 것이다.

2. ClojureScript 컴파일 옵션 :static-fns

클로저스크립트 컴파일시 :static-fns 옵션을 true로 주면, Static Dispatch가 이루어져 함수 실행 속도가 상당히 빨라진다. 단, 이 옵션은 release 버전을 컴파일할 때에만 true로 지정해 주는 것이 좋다. 개발 버전에서는 개발의 편의를 위해 false(default 값)를 사용하는 것이 바람직하다.

참고로, 클로저스크립트(예: org.clojure/clojurescript "1.8.40") 자체는 :static-fns 옵션 지정 여부와 상관 없이, 항상 :static-fns true로 컴파일된다.

이 옵션은 다음과 같은 형식으로 준다.

project.clj
(defproject foo ......
  :cljsbuild {:builds [{:id "release"
                        :source-paths ["src/cljs"]
                        :compiler {:output-to "resources/public/js/main.js"
                                   :output-dir "resources/public/js/out"
                                   :asset-path "js/out"
                                   :main "foo.core"
                                   :optimizations :advanced
                                   :static-fns true   (1)
                                   :pretty-print falue}}]})

예를 들어, 다음과 같은 코드가 있다고 하자.

(defn foo
  ([] "foo")
  ([x] x)
  ([x y] y))

(foo)
(foo 1)
(foo 1 2)
(foo 1 2 3)

2.1. :static-fns false 컴파일

이 코드가 :static-fns false 옵션을 주고 컴파일된 자바스크립트 코드는 다음과 같다.

cljs.user.foo = (function cljs$user$foo(var_args){   (2)
  var args22 = [];
  var len__27274__auto___25 = arguments.length;
  var i__27275__auto___26 = (0);
  while(true){
    if((i__27275__auto___26 < len__27274__auto___25)){
      args22.push((arguments[i__27275__auto___26]));
      var G__27 = (i__27275__auto___26 + (1));
      i__27275__auto___26 = G__27;
      continue;
    }
    else {
    }
    break;
  }
  var G__24 = args22.length;
  switch (G__24) {
    case (0):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$0();
      break;
    case (1):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$1((arguments[(0)]));   (3)
      break;
    case (2):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$2((arguments[(0)]),(arguments[(1)]));
      break;
    default:
      throw (new Error([cljs.core.str("Invalid arity: "),cljs.core.str(args22.length)].join('')));
  }
}
                );
cljs.user.foo.cljs$core$IFn$_invoke$arity$0 = (function (){
  return "foo";
}
                                              );
cljs.user.foo.cljs$core$IFn$_invoke$arity$1 = (function (x){   (4)
  return x;
}
                                              );
cljs.user.foo.cljs$core$IFn$_invoke$arity$2 = (function (x,y){
  return y;
}
                                              );
cljs.user.foo.cljs$lang$maxFixedArity = (2);
cljs.user.foo.call(null);
cljs.user.foo.call(null,(1));      (1)
cljs.user.foo.call(null,(1),(2));
cljs.user.foo.call(null,(1),(2),(3));

컴파일된 코드가 좀 장황하기는 하지만, 핵심적인 내용은 <1>의 함수가 호출되는 경우, <2>를 경유해 <3>을 거쳐 <4>를 호출하게 된다는 것이다. 이 작업이 run-time에 일어나므로(Dynamic Dispatch) 그만큼 함수 호출 시간이 길어지게 된다.

2.2. :static-fns true 컴파일

반면에 :static-fns true 옵션을 주고 컴파일된 코드는 다음과 같다.

cljs.user.foo = (function cljs$user$foo(var_args){   (2)
  var args22 = [];
  var len__27274__auto___25 = arguments.length;
  var i__27275__auto___26 = (0);
  while(true){
    if((i__27275__auto___26 < len__27274__auto___25)){
      args22.push((arguments[i__27275__auto___26]));
      var G__27 = (i__27275__auto___26 + (1));
      i__27275__auto___26 = G__27;
      continue;
    }
    else {
    }
    break;
  }
  var G__24 = args22.length;
  switch (G__24) {
    case (0):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$0();
      break;
    case (1):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$1((arguments[(0)]));   (3)
      break;
    case (2):
      return cljs.user.foo.cljs$core$IFn$_invoke$arity$2((arguments[(0)]),(arguments[(1)]));
      break;
    default:   (6)
      throw (new Error([cljs.core.str("Invalid arity: "),cljs.core.str(args22.length)].join('')));
  }
}
                );
cljs.user.foo.cljs$core$IFn$_invoke$arity$0 = (function (){
  return "foo";
}
                                              );
cljs.user.foo.cljs$core$IFn$_invoke$arity$1 = (function (x){   (4)
  return x;
}
                                              );
cljs.user.foo.cljs$core$IFn$_invoke$arity$2 = (function (x,y){
  return y;
}
                                              );
cljs.user.foo.cljs$lang$maxFixedArity = (2);
cljs.user.foo.cljs$core$IFn$_invoke$arity$0();
cljs.user.foo.cljs$core$IFn$_invoke$arity$1((1));   (1)
cljs.user.foo.cljs$core$IFn$_invoke$arity$2((1),(2));
cljs.user.foo((1),(2),(3));   (5)

잘 살펴 보면, :static-fns false 옵션으로 컴파일한 코드와 실제로 차이나는 곳은 <1>을 전후한 부분 뿐이다. 이렇게 컴파일된 코드에서는 <1>이 호출되면 <2>와 <3>을 경유하지 않고 곧바로 <4>를 호출하게 컴파일 된다. 이렇게 compile-time에 어떤 함수를 호출할지를 미리 결정(Static Dispatch)함으로써, 함수 호출 시간이 그만큼 줄어들게 되어 실행 속도 향상의 효과를 얻을 수 있게 된다.

참고로, <5>의 경우에는 세 개의 인수를 갖는 함수가 정의되어 있지 않으므로, <2>의 경로를 거치는 Dynamic Dispatch를 수행하게 되는데, 결국 <6>에 이르러 예외를 발생시키게 된다.