Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why using a class from a typed/racket module in a untyped one yields bad performance?

See EDIT 1, 2, and 3 for updates. I leave here the complete research process.

I know we can use typed/racket modules from untyped racket (and vice versa). But when doing so, the typed/racket module just behaves as if it was typed/racket/no-check, which disables optimizations and just uses it as a normal untyped module.

For example, if you have a typed/racket module like this:

#lang typed/racket
(require math)
(provide hello)
(define (hello [str : String])
  (define result : (Matrix Flonum) (do-some-crazy-matrix-operations))
  (display (format "Hello ~a! Result is ~a" str result)))

And you want to use it in an untyped program like this:

#lang racket/base
(require "hello-matrix.rkt")
(hello "Alan Turing")

You'll get pretty bad performance results (in my case, I'm doing about 600000 matrix multiplications, the program doesn't even finish), while using #lang typed/racket makes my program finish in 3 seconds.

The downside is that my untyped code becomes infected with types, forcing me to write all my program in TR, turning me crazy pretty quickly.

But my savior was not so far away. I stumbled upon a funny April's-fool-like package Jay McCarthy wrote by a cloudy dark night, called live-free-or-die, which pretty much does this:

http://docs.racket-lang.org/live-free-or-die/index.html

#lang racket/base
(require (for-syntax racket/base
                     typed-racket/utils/tc-utils))
(define-syntax (live-free-or-die! stx)
  (syntax-case stx ()
    [(_)
     (syntax/loc stx
       (begin-for-syntax
         (set-box! typed-context? #t)))]))
(provide live-free-or-die!
         (rename-out [live-free-or-die!
                      Doctor-Tobin-Hochstadt:Tear-down-this-wall!]))

By using it in my typed/racket module, like so:

#lang racket
(require live-free-or-die)
(live-free-or-die!)
(require math)
(provide hello)
(define (hello str)
  (define result (do-some-crazy-matrix-operations))
  (display (format "Hello ~a! Result is ~a" str result)))

Now my module is not #lang typed/racket anymore, but the results of running it are spectacular! It runs in 3 seconds, exactly as if it was a typed/racket module.


I am, of course, disgusted with that hack, and that's why I'm wondering if there could be a better solution to this, especially for making the matrix operations from math usable.

The Google Groups discussion about that crazy module Jay wrote is the only piece of information I could get.

https://groups.google.com/forum/#!topic/racket-users/JZoHYxwwJqU

People in this thread seems to say that the module is not useful anymore:

Matthias Felleisen
Well, now that our youngsters have easily debunked the package, we can let it die because it no longer wants to live.

Is there really a better alternative?


EDIT 1 - A testable example

If you want to test the performance difference, try using this definition of do-some-crazy-matrix-operations:

#lang typed/racket
(require math)
(provide hello)

(: do-some-crazy-matrix-operations : (-> (Matrix Flonum)))
(define (do-some-crazy-matrix-operations)
  (define m1 : (Matrix Flonum) (build-matrix 5 5 (lambda (x y) (add1 (random)))))
  (define m2 : (Matrix Flonum) (build-matrix 5 5 (lambda (x y) (add1 (random)))))
  (for ([i 60000])
    (set! m1 (matrix-map * m1 m2))
    (set! m2 (matrix-map * m1 m2)))
  (matrix+ m1 m2))

(define (hello [str : String])
  (define result : (Matrix Flonum) (do-some-crazy-matrix-operations))
  (display (format "Hello ~a! Result is ~a" str result)))

(time (hello "Alan Turing"))

Using #lang typed/racket it runs in 288ms:

cpu time: 288 real time: 286 gc time: 16

Using #lang typed/racket/no-check it runs in 52 seconds:

cpu time: 52496 real time: 52479 gc time: 396

Using #lang racket and live-free-or-die it runs in 280ms:

cpu time: 280 real time: 279 gc time: 4

EDIT 2 - This was not the issue!

Following John Clement's answer, I discovered the examples were not enough to reproduce the real issue. Using typed/racket modules in untyped ones actually works fine.

My real problem is an issue with the boundary contracts created by a class that passes from untyped to typed racket.

Let's consider this implementation of hello-matrix.rkt:

#lang typed/racket
(require math)
(provide hello crazy% Crazy)

(define-type CrazyClass (Class (field [m1 (Matrix Flonum)])
                               (field [m2 (Matrix Flonum)])
                               (do (-> (Matrix Flonum)))))
(define-type Crazy (Instance CrazyClass))
(: crazy% CrazyClass)
(define crazy%
  (class object%
    (field [m1 (build-matrix 5 5 (lambda (x y) (add1 (random))))]
           [m2 (build-matrix 5 5 (lambda (x y) (add1 (random))))])

    (super-new)

    (define/public (do)
      (set! m1 (matrix* (matrix-transpose m1) m2))
      (set! m2 (matrix* (matrix-transpose m1) m2))
      (matrix+ m1 m2))))

(: do-some-crazy-matrix-operations : Crazy -> (Matrix Flonum))
(define (do-some-crazy-matrix-operations crazy)
  (for ([i 60000])
    (send crazy do))
  (matrix+ (get-field m1 crazy) (get-field m2 crazy)))

(define (hello [str : String] [crazy : Crazy])
  (define result : (Matrix Flonum) (do-some-crazy-matrix-operations crazy))
  (display (format "Hello ~a! Result is ~a\n" str result)))

Then those two usages:

#lang typed/racket
(require "hello-matrix.rkt")
(define crazy : Crazy (new crazy%))
(time (hello "Alan Turing" crazy))

cpu time: 1160 real time: 1178 gc time: 68

#lang racket
(require "hello-matrix.rkt")
(define crazy (new crazy%))
(time (hello "Alan Turing" crazy))

cpu time: 7432 real time: 7433 gc time: 80

Using contract-profile:

Running time is 83.47% contracts
6320/7572 ms

BY CONTRACT

g66 @ #(struct:srcloc hello-matrix.rkt 3 15 50 6)
  3258 ms

(-> String (object/c (do (-> any/c (struct/c Array (vectorof Index) Index (box/c (or/c #f #t)) (-> Void) (-> (vectorof Index) Float)))) (field (m1 (struct/c Array (vectorof Index) Index (box/c (or/c #f #t)) (-> Void) (-> (vectorof Index) Float))) (m2 (struct/c Array (vectorof Index) Index (box/c (or/c #f #t)) (-> Void) (-> (vectorof Index) Float))))) any) @ #(struct:srcloc hello-matrix.rkt 3 9 44 5)
  3062 ms

EDIT 3 - Passing struct from typed to untyped is more performant than passing class

Using a struct instead of a class fixes this:

hello-matrix.rkt:

#lang typed/racket
(require math)
(provide hello (struct-out crazy))

(struct crazy ([m1 : (Matrix Flonum)] [m2 : (Matrix Flonum)]) #:mutable)
(define-type Crazy crazy)

(define (crazy-do [my-crazy : Crazy])
  (set-crazy-m1! my-crazy (matrix* (matrix-transpose (crazy-m1 my-crazy))
                                   (crazy-m2 my-crazy)))
  (set-crazy-m2! my-crazy (matrix* (matrix-transpose (crazy-m1 my-crazy))
                                   (crazy-m2 my-crazy)))
  (matrix+ (crazy-m1 my-crazy) (crazy-m2 my-crazy)))

(: do-some-crazy-matrix-operations : Crazy -> (Matrix Flonum))
(define (do-some-crazy-matrix-operations my-crazy)
  (for ([i 60000])
    (crazy-do my-crazy))
  (matrix+ (crazy-m1 my-crazy) (crazy-m2 my-crazy)))

(define (hello [str : String] [my-crazy : Crazy])
  (define result : (Matrix Flonum) (do-some-crazy-matrix-operations my-crazy))
  (display (format "Hello ~a! Result is ~a\n" str result)))

Usage:

#lang typed/racket
(require "hello-matrix.rkt")
(require math)
(define my-crazy (crazy (build-matrix 5 5 (lambda (x y) (add1 (random))))
                        (build-matrix 5 5 (lambda (x y) (add1 (random))))))
(time (hello "Alan Turing" my-crazy))

cpu time: 1008 real time: 1008 gc time: 52

#lang racket

cpu time: 996 real time: 995 gc time: 52

like image 524
Zoé Martin Avatar asked Oct 28 '22 22:10

Zoé Martin


1 Answers

I'm writing this as an "answer" to allow me to format my code... I think we're talking a bit past each other. Specifically, I can run your typed code from an untyped module in about half a second. I named your typed code file "hello-matrix.rkt", as you suggested, and then ran the untyped module that you provided (the one that required the TR module) and it took the same amount of time (about half a second). Let me be careful in saying this:

Contents of "hello-matrix.rkt":

#lang typed/racket
(require math)
(provide hello)

(: do-some-crazy-matrix-operations : (-> (Matrix Flonum)))
(define (do-some-crazy-matrix-operations)
  (define m1 : (Matrix Flonum) (build-matrix 5 5 (lambda (x y) (add1 (random)))))
  (define m2 : (Matrix Flonum) (build-matrix 5 5 (lambda (x y) (add1 (random)))))
  (for ([i 60000])
    (set! m1 (matrix-map * m1 m2))
    (set! m2 (matrix-map * m1 m2)))
  (matrix+ m1 m2))

(define (hello [str : String])
  (define result : (Matrix Flonum) (do-some-crazy-matrix-operations))
  (display (format "Hello ~a! Result is ~a" str result)))

(time (hello "Alan Turing"))

then, I call it from an untyped module, just like you said:

#lang racket/base
(require "hello-matrix.rkt")
(time (hello "Alan Turing"))

and here's the result:

Hello Alan Turing! Result is (array #[#[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0]])
cpu time: 719 real time: 710 gc time: 231
Hello Alan Turing! Result is (array #[#[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0] #[+inf.0 +inf.0 +inf.0 +inf.0 +inf.0]])
cpu time: 689 real time: 681 gc time: 184

That is, it takes the same time to call it from untyped racket that it does from typed racket.

This result might depend a bit on what version of DrRacket you're using; I'm using 6.11.

All of this is to demonstrate that TR code is still TR code, even if you call it from untyped code. I do believe that you're having performance problems, and I do believe that they're related to matrix operations, but this particular example doesn't illustrate them.

like image 182
John Clements Avatar answered Nov 18 '22 10:11

John Clements