fasterthanlime / shin

:warning: (def shin (dissoc clojurescript :jvm :google_closure)) (deprecated)
MIT License
35 stars 1 forks source link

Improve macro performance by shunning serialization cycle #98

Closed fasterthanlime closed 9 years ago

fasterthanlime commented 9 years ago

Macro expansion has been the black sheep of shin compilation speed for a while now (uh.. two weeks).

While e99dd94bc098fe8cb7ea1722f9576db23bc332a3 and 78c4d698c47e4844ee53a0f260e72234351cacd2 helped a lot (reduced tests from 7s to ~2.3s), I still have another idea.

Right now, if you have:

(when cond huge-body)

Then both cond and huge-body are passed as ClojureScript data structures like lists, vectors, maps, keywords, symbols, as if the macro was invoked as a function, like this:

(when `cond `huge-body)

There are several performance problems with that:

So even though huge-body will not change one bit, it'll be completely thrown away and reparsed just by virtue of passing through a macro.

This is sub-optimal. Instead, we can use V8/Ruby integration better.

Here's a proof of concept:

require 'shin/compiler'
require 'shin/js_context'
require 'shin/ast'

opts = {}
compiler = Shin::Compiler.new(opts)
core_path = "../shin/lib/shin/cljs/cljs/core.cljs"
core = compiler.compile(File.read(core_path))

ctx = Shin::JsContext.new
ctx.providers << compiler
ctx.load("cljs.core")

c = ctx.context

t = Shin::AST::Token.new("meh", 0)
s1, s2, s3 = %w(lloyd florida cancun).map { |x| Shin::AST::Symbol.new(t, x) }
l = Shin::AST::List.new(t, Hamster.vector(s1, s2, s3))

res = c.eval(%Q{
  var core = $kir.modules["cljs.core"].exports;
  var c = core.cons,      s = core.symbol,
      k = core.keyword,   l = core.list,
      h = core.hash$_map, v = core.vector;
  (function (input) {
    return core.interleave(
       c(k("noah"),
         c(s("lena"), core.next(input))),
       l(1, v(2, 3), h(4, 5, 6, 7)));
  }) })
res = res.call(l)
pr_str = c.eval("$kir.modules['cljs.core'].exports.pr$_str")
elm = nil

while res
  case res
  when V8::Object
    elm = res["$_first$$arity1"].methodcall(res, res)
    res = res["$_next$$arity1"].methodcall(res, res)
  else
    elm = res.send(:'$_first$$arity1', res)
    res = res.send(:'$_next$$arity1', res)
  end

  case elm
  when V8::Object
    puts "(V8) elm = #{pr_str.call(elm)}"
  else
    puts "(Ry) elm = #{elm}"
  end
end

For this to work, ast.rb has to be modified, a bit:

# *snip*
    class List < Sequence
      def list?
        true
      end

      def to_s
        if ENV['PRETTY_SEXP']
          "\n(#{inner.map(&:to_s).join("\n")})".split("\n").map do |x|
            "  " + x
          end.join("\n")
        else
          "(#{inner.map(&:to_s).join(" ")})"
        end
      end

      define_method(:'$_seq$$arity1') do |s|
        puts "[AST::List] Calling -seq"
        if inner.empty?
          nil
        else
          self
        end
      end

      define_method(:'$_first$$arity1') do |s|
        puts "[AST::List] Calling first"
        inner.first
      end

      define_method(:'$_next$$arity1') do |s|
        puts "[AST::List] Calling next"
        if inner.count > 1
          List.new(token, inner.drop(1))
        else
          nil
        end
      end

      define_method(:'$_rest$$arity1') do |s|
        puts "[AST::List] Calling rest"
        List.new(token, inner.drop(1))
      end

      def [](x)
        puts "[AST::List] asking for #{x}"
        return true if %w(cljs$dcore$vINext cljs$dcore$vISeq cljs$dcore$vISeqable).include?(x)
        nil
      end
    end

# *snip*

This will:

However, it's not an easy thing to get to work properly. Gonna have to write a few good tests to make sure it's solid before mutator can start using it. Hopefully though, the performance gains are going to be sizable.

fasterthanlime commented 9 years ago

Ongoing work in happening in branch fast-macros.

fasterthanlime commented 9 years ago

Even though it was harder than anticipated to gain performance from this approach, it's now faster than the old one was. Along with a few dozen other optimizations/simplifications to the codebase, it's been merged into master and I'm now free to tackle #99, at last!