LIPS-scheme / lips

Scheme based powerful lisp interpreter in JavaScript
https://lips.js.org
Other
422 stars 35 forks source link

Investigate and document performance #82

Open jcubic opened 4 years ago

jcubic commented 4 years ago

Here is example of slow and fast code for calculating factorial of 1000:

;; slow
(--> (new Array 1000) (fill 0) (map (curry (n-ary 3 +) 1)) (reduce (binary *)))

;; same but faster
(--> (new Array 1000) (fill 0) (map (lambda (_ i) (+ i 1))) (reduce (lambda (a b) (* a b))))

;; faster and simpler
(apply * (cdr (range 1001)))
jcubic commented 3 years ago

Speed test, array based code is much faster even that that is lambda that invoke the interpreter in each iteration:

(define (get-zeros data)
  (--> data
       (split "\n")
       (filter (lambda (line)
                  (line.match #/ZERO;Nd;/
                              )))
       (map (lambda (line)
               (let ((parts (line.split ";")))
                 (number->string (string->number (. parts 0) 16)))))))

(define (get-zeros data)
  (let* ((lines (data.split "\n"))
         (re #/ZERO;Nd;/
            )
         (len (vector-length lines))
         (result (vector)))
    (do ((i 0 (+ i 1)))
      ((>= i len) result)
      (let ((line (. lines i)))
        (if (not (null? (line.match re)))
            (let* ((parts (line.split ";"))
                   (number (number->string (string->number (. parts 0) 16))))
              (result.push number)))))))
jcubic commented 3 years ago

The different is huge Simple array methods:

real    0m2,536s
user    0m3,113s
sys 0m0,094s

do macro:

real    0m44,835s
user    0m45,418s
sys 0m0,133s

Probably each lambda slow down the execution, so less code is always better. Also using just JavaScript code in Scheme would probably be the fastest.

Example modifying the function to use JS functions:

(define (get-zeros data)
  (--> data
       (split "\n")
       (filter (lambda (line)
                  (line.match #/ZERO;Nd;/
                              )))
       (map (lambda (line)
               (let ((parts (line.split ";")))
                 (--> (parseInt (. parts 0) 16) (toString)))))))

There is small improvements but there only

real    0m2,503s
user    0m3,063s
sys 0m0,103s

Defining regex outside of function:

real    0m2,475s
user    0m3,037s
sys 0m0,099s

Those are not reliable, benchmark need to be run multiple times and use average to be representative.

jcubic commented 3 years ago

This is probably the fastest function:

(define (get-zeros data)
  (let ((re #/ZERO;Nd;/
            ))
    (--> data
         (split "\n")
         (filter (lambda (line)
                   (line.match re)))
         (map (lambda (line)
                (let* ((parts (line.split ";"))
                       (number (parseInt (. parts 0) 16)))
                  (number.toString)))))))

Only native functions and no macros.

real    0m2,478s
user    0m3,040s
sys 0m0,078s

When there will be macro expansion time it will allow to use Scheme based macros.

jcubic commented 3 years ago

I should probably use some benchmarking tool to test performance: This SO question have example of using Benchmark.js. How to profile Javascript now that JSPerf is down?

But the usage of this tools is simple https://benchmarkjs.com/

jcubic commented 3 years ago

Literals regexes are slightly faster, probably because they are created in parser and getting value from scope chain is slower.

>>> Regex: variable x 28.59 ops/sec ±2.58% (51 runs sampled)
>>> Regex: literal x 29.99 ops/sec ±2.52% (53 runs sampled)
Fastest is Regex: literal