Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

playground examples don't work with --scheme-numbers enabled #327

Open
stchang opened this issue May 25, 2023 · 14 comments
Open

playground examples don't work with --scheme-numbers enabled #327

stchang opened this issue May 25, 2023 · 14 comments

Comments

@stchang
Copy link
Member

stchang commented May 25, 2023

Still investigating but I'm frequently getting this (codemirror?) error: Uncaught TypeError: Cannot read properties of undefined (reading 'chunkSize')

@kclapper
Copy link
Collaborator

That seems like an error from the jsnumbers library having to do with it's implementation of bigints. What playground example are you getting that error with? It looks like most of the examples break with --scheme-numbers in various ways.

@stchang
Copy link
Member Author

stchang commented May 25, 2023

I'm still investigating but the examples themselves actually seem ok (I ran the ones I could offline). So the error must happen while it's getting loaded

@stchang
Copy link
Member Author

stchang commented May 25, 2023

The previous error I posted was a red herring i think.

It looks like the actual problem is due to scheme numbers getting passed to js functions

Here is a basic example that does not render properly in the playground

#lang racketscript/base

(require racketscript/htdp/image)

(print-image (text "RacketScript" 48 'black))

What is happening is that the compilation of print-image (from racketscript/htdp/image) has a call to js canvas translate method where the arguments are scheme numbers:

  • without --scheme-numbers: ctx1369.translate(d1367.width / 2, d1367.height / 2);
  • with --scheme-numbers: ctx1369.translate(M1.__by_(d1367.width, 2), M1.__by_(d1367.height, 2));

I think this shows that --scheme-numbers may be less useful than we thought for programs that interop with javascript. Unless we can come up with a more fine-grained way to enable/disable it.

@stchang
Copy link
Member Author

stchang commented May 25, 2023

For completeness, here's what a proper/improper rendering of the above example looks like

  • without --scheme-numbers
    image
  • with --scheme-numbers
    image

@stchang
Copy link
Member Author

stchang commented Jul 5, 2023

Update:
Attempting to implement Symbol.toPrimitive for all number representations (see kclapper/tower.js@5d7c846) to address the above issue.

Still unconfirmed whether it worked, but we ran into a variation of the issue (sort of the dual to the above).
Specifically, js-racketscript interaction also sometimes results in unboxed numbers getting passed back to scheme-number functions.

Currently in this situation, they are treated as exact integers (the current design decision comes from the original https://github.com/dyoo/js-numbers library) (https://github.com/dyoo/js-numbers/blob/2cf776f6017858a30488a4ec768be3e67bb0a6c4/src/js-numbers.js#L29-L31):

// We try to stick with the unboxed fixnum representation for
// integers, since that's what scheme programs commonly deal with, and
// we want that common type to be lightweight.

But in racketscript, the unboxed number does not have to be an integer and currently this scenario is unhandled by the original library, which can cause problems

To address this gap, here is our new proposed treatment of unboxed numbers that flow back into the numbers library:

  • BigInt: treated as exact but, for performance, gets converted to plain js number if < Number.MAX_SAFE_INTEGER
    • microbenchmarks show bigint is 10x slower than plain nums for small ints
    • the above conversion is only 2x slower
  • non BigInt: treated as inexact real

@kclapper Please clarify if needed

@kclapper
Copy link
Collaborator

kclapper commented Jul 6, 2023

It occurred to me that using BigInt to represent exact numbers could be problematic when it comes to interacting with other Javascript libraries. Most Javascript libraries won't expect a BigInt as numeric input and they won't be automatically coerced. So if we want interop to be mostly automatic, it may be better to always return boxed numbers from tower.js functions. The boxed numbers will automatically coerce themselves to numbers when fed to arithmetic operators.

I'll take a look at how much slower it would be. The actual implementation of the functions can unbox them where appropriate so hopefully the slow down isn't terrible.

@stchang
Copy link
Member Author

stchang commented Jul 6, 2023

Good point. Why dont we just do it the straightforward way first (use isInteger and < MAX_SAFE_INTEGER) and see how the performance is before trying to optimize

@kclapper
Copy link
Collaborator

kclapper commented Jul 6, 2023

I think I'm not quite sure what you mean. Tower.js already turns BigInts into numbers if they're small enough.

I think the issue is that, if tower.js functions return a BigInt, then it'll causes errors if they're fed into another Javascript library.

I did a few microbenchmarks to see if there's a significant slowdown if tower.js functions always return the boxed representation instead, it doesn't appear to have a significant impact (because the implementation unboxes it when possible before doing the computation anyway).

So I'd suggest we revise our treatment of unboxed numbers to this:

For input to a tower.js function:

  • BigInt: treated as an exact integer (but possibly converted to plain number where possible for performance reasons)
  • number: treated as an inexact real number
  • Boxed numbers: unboxed if possible, otherwise left as is.

For output from a tower.js function:

  • Never return BigInt to avoid accidental type errors
  • number: returned if the result is an inexact real number
  • Boxed numbers: returned in all other cases. Boxed numbers can be automatically coerced to number or string as appropriate when encountered by another Javascript library

In the implementations of the tower.js functions, boxed numbers will be unboxed where possible so they execute on a fast path.

@stchang
Copy link
Member Author

stchang commented Jul 6, 2023

I guess I was suggesting that numbers be output for small enough exact integers, in which case we would need to check any input numbers to distinguish whether it was exact or inexact. Would that not be feasible?

@kclapper
Copy link
Collaborator

kclapper commented Jul 6, 2023

Oh I see. This is feasible but we'd decided it was undesirable. The only way to distinguish between an exact and inexact number would be to see if it's an integer. So we'd be in a situation where unboxed numbers are all inexact, unless they just happen to be integers.

I think my suggestion would actually work out fairly well. Since all Inexact numbers are being returned as boxed numbers right now, we may end up with fewer boxed values floating around anyway (for computations using a lot of inexact numbers).

@stchang
Copy link
Member Author

stchang commented Jul 6, 2023

Ok sounds good. Let's try your proposal

@kclapper
Copy link
Collaborator

kclapper commented Jul 10, 2023

Edit: I was incorrect, these benchmarks are with bigints as the exact integer representation, number as the inexact real representation, and everything else boxed.

With micro-benchmarks on the expt function, this unboxed representation is on par with js-numbers and in most cases is faster. The worst case has tower.js ~2x slower than js-numbers.

Exact complex, expt(100+89i, 5000)
--------------------------------------------------------------------------------
js-numbers: 	7.4032404065132145 ms/trial 	(740.3240406513214 total)
tower.js: 	0.3210354065895081 ms/trial 	(32.103540658950806 total)
Results match: true

Large Exact integer, expt(1000, 5000)
--------------------------------------------------------------------------------
js-numbers: 	5.449012107849121 ms/trial 	(544.9012107849121 total)
tower.js: 	0.09844708204269409 ms/trial 	(9.84470820426941 total)
Results match: true

Small Exact Base and Exp, expt(2, 30)
--------------------------------------------------------------------------------
js-numbers: 	0.0003891992568969727 ms/trial 	(0.038919925689697266 total)
tower.js: 	0.0008091330528259277 ms/trial 	(0.08091330528259277 total)
javascript: 	0.0002779674530029297 ms/trial 	(0.02779674530029297 total)
Results match: true

Small Inexact Base and Exp, expt(5.5, 5.5)
--------------------------------------------------------------------------------
js-numbers: 	0.0019262123107910156 ms/trial 	(0.19262123107910156 total)
tower.js: 	0.0006387543678283692 ms/trial 	(0.06387543678283691 total)
javascript: 	0.0002837491035461426 ms/trial 	(0.028374910354614258 total)
Results match: true

Mixed Precision, expt(5.5, 10)
--------------------------------------------------------------------------------
js-numbers: 	0.0006833291053771973 ms/trial 	(0.06833291053771973 total)
tower.js: 	0.0006029105186462402 ms/trial 	(0.06029105186462402 total)
javascript: 	0.0002796030044555664 ms/trial 	(0.02796030044555664 total)
Results match: true

To better rule out garbage collection time, I force Node to garbage collect before it runs trials on each case (js-numbers vs tower.js vs javascript). Right now, it's running 100 trials per case. If you force Node to garbage collect between each trial within a case instead, tower.js is faster every time. This likely means tower.js is allocating more objects than js-numbers.

@kclapper
Copy link
Collaborator

Here is the same benchmark but with the unboxed number representation I suggested. To recap:

For input to a tower.js function:

  • BigInt: treated as an exact integer (but possibly converted to plain number where possible for performance reasons)
  • number: treated as an inexact real number
  • Boxed numbers: unboxed if possible, otherwise left as is.

For output from a tower.js function:

  • Never return BigInt to avoid accidental type errors
  • number: returned if the result is an inexact real number
  • Boxed numbers: returned in all other cases (better for plain JS interop)

Benchmark summary:

  • With big numbers tower.js is at least an order of magnitude faster
  • With exact integers tower.js is about 3x slower than js-numbers
  • With inexact numbers tower.js is about 2x faster than js-numbers
  • In other cases tower.js and js-numbers are about comparable

Tower.js gives us better interop with plain JS at the cost of speed in the case of exact integers. It also gives us faster big number and inexact number computations. This is an isolated benchmark, so I'll need to see if these results hold in other cases as well.

Exact complex, expt(100+89i, 5000)
--------------------------------------------------------------------------------
js-numbers: 	7.069923729896545 ms/trial 	(706.9923729896545 total)
tower.js: 	0.31644707202911376 ms/trial 	(31.644707202911377 total)
Results match: true

Large Exact integer, expt(1000, 5000)
--------------------------------------------------------------------------------
js-numbers: 	5.316285834312439 ms/trial 	(531.6285834312439 total)
tower.js: 	0.09724792003631592 ms/trial 	(9.724792003631592 total)
Results match: true

Small Exact Base and Exp, expt(2, 30)
--------------------------------------------------------------------------------
js-numbers: 	0.00040170669555664065 ms/trial 	(0.04017066955566406 total)
tower.js: 	0.0011629199981689454 ms/trial 	(0.11629199981689453 total)
javascript: 	0.0002729034423828125 ms/trial 	(0.02729034423828125 total)
Results match: true

Small Inexact Base and Exp, expt(5.5, 5.5)
--------------------------------------------------------------------------------
js-numbers: 	0.002095460891723633 ms/trial 	(0.20954608917236328 total)
tower.js: 	0.0007837152481079101 ms/trial 	(0.07837152481079102 total)
javascript: 	0.00026997089385986326 ms/trial 	(0.026997089385986328 total)
Results match: true

Mixed Precision, expt(5.5, 10)
--------------------------------------------------------------------------------
js-numbers: 	0.0007716655731201172 ms/trial 	(0.07716655731201172 total)
tower.js: 	0.0007920598983764648 ms/trial 	(0.07920598983764648 total)
javascript: 	0.0002725410461425781 ms/trial 	(0.027254104614257812 total)
Results match: true

@kclapper
Copy link
Collaborator

Turns out this scheme is pretty slow for common fast functions like multiply and divide. So instead we'll use a representation that's the opposite of js-numbers.

  • inexact numbers: unboxed javascript numbers
  • exact numbers: boxed

No use of bigint's other than for representing big boxed numbers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants