Why is ruby so slow vs. Javascript in Rhodes application ?

Hello everybody,

We are developing a Rhodes application with a lot of images processing and we had to do some calculations in Javascript because our graphics lib is a Javascript one.

As these calculations took too much time for us, we tried to implement a basic benchmark test in Ruby in order to compare calculation performance.

The benchmark consists in building 2 matrices (1000x1000) with random data and build a third one with a simple M3(x,y) = M1(x,y)*M2(x,y)/255.

Here are the results we got with Rhodes 5.0.30 :

Rhosimulator : Ruby : 194 ms, Javascript : 58 ms

Android : Ruby : 871 ms, Javascript : 259 ms

Windows 8.1 : Ruby : 1801 ms, Javascript : 426 ms

iPhone6 : Ruby : 1700 ms (!!!), Javascript : 134 ms

We really thought Ruby should be faster as, if we understand well, Ruby code is compiled into Ruby-bytecode and then executed by the Rhodes rubyVM interpreter.

So my question is : is Javascript really always faster than Ruby in a Rhodes application and does it mean we should always prefer Javascript when it comes to massive calculation ?

Thanks,

Thierry

Robert Galvin
Hi Thierry,Any chance you can

Hi Thierry,

Any chance you can share your app code? I am trying to better understand exactly how you are performing the test?  What version of Android were you using? Was RhoSim running on Mac or Windows?

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Thierry Leloup
Hi Rob,We have tested with

Hi Rob,

We have tested with Android 4.4 and Rhosimulator was running on a Mac Book Pro (i7/8GB).

Our javascript (size = 1,000,000):

function testLoop(size) {

  var firstArray = [], secondArray = [];

  for (var i = 0; i < size; i++) {

  firstArray[i] = Math.round(Math.random() * 255);

  secondArray[i] = Math.round(Math.random() * 255);

  }

  var resultArray = [];

  var begin = Date.now();

  for (var i = 0; i < size; i++) {

  resultArray[i] = firstArray[i] * secondArray[i] / 255;

  }

  consoleLog("[JavaScript] Duration with array of " + size + " elements : " + (Date.now() - begin));

}

======================================================================

And Ruby code (size = 1,000,000) :

def testLoop

  @size = @params['size'] || 100
  if @size.is_a? String
  @size = @size.to_i
  end

  firstArray = Array.new(@size) {|i| (rand()*255).round() }

  secondArray = Array.new(@size) {|i| (rand()*255).round() }

  resultArray = Array.new(@size)

  beginDur = Time.now

  for i in 0..@size-1
  resultArray[i] = firstArray[i] * secondArray[i] / 255
  end

  duration = Time.now - beginDur
  Rho::Log.info("[Ruby] Duration with array of #{@size} elements : #{duration}", "DEV")

end

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
Did you test the Javascript

Did you test the Javascript in Rhodes, or in the system browser? The system browser on all of these platforms does JIT-compile to real machine code... So, make sure you do you JS tests inside of a Rhodes app to compare apples with apples!

Have you tried any standard benchmark on both?

Aside: On iOS, (8.0+) WKWebView will now JIT-compile JS as well. But Rhodes doesn't have it yet. It still uses only UIWebView, which does not JIT-compile. So, in the future, you may get even better math performance in JS on iOS.

If you have a specific need that is worth the effort, you could drop down to C (which is available on every Rhodes platform) and write a Rhodes extension to do your math.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
Oh, I see from your first

Oh, I see from your first post, you are doing image processing. And you are using some JS graphics library, and finding the performance lacking, so hoping you might do it faster with Ruby...

It's unclear what you mean by "some calculations". Is there some specific image-processing algorithm you need to use?

All modern platforms have great native image-processing libraries built-in that will leverage the graphics processor chip. So, it might be worth-while for you to write a native extension to make use of these. But I don't know how many platforms you need to support - it might be a pain if you have to support several!

This would likely give you hundreds of times your current performance.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
One little thing you can try

One little thing you can try in your little benchmark is to make sure you use float constants - e.g. 255.0 - so that run-time code doesn't have to convert integers to floats. Though I suppose not a huge impact.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Thierry Leloup
In fact, we use several

In fact, we use several Javascript image processing libraries (PaperJS, CamanJS) and added some algorithms by our own because some of the processing were not working on Win32, for instance. All of these are done in Javascript.

We need a support for Android, iOs and Win32 and already had to mix some part of these librairies in order for our application to work on these 3 OS...

If we decide the general performance for this function is not good enough, we will turn ourself to some native development but it will add a lot of time.

Thanks for your answer, Jon.

Thierry

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Pietro Francesc...
Hi Thierry,as Jon already

Hi Thierry,

as Jon already pointed out, the browser engine on most of modern platform includes a JIT JavaScript VM. Ruby VM is not JIT and, especially in loops, you can see some noticeable difference.

If what you're looking for is maximum performance, reimplementing the function natively with a C shared codebase can be the best option in term of speed and "easy" of support.

~Pietro

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Robert Galvin
Does this loop represent how

Does this loop represent how your code is behaving? Or are just using it as a benchmark type of test. As Pietro and Jon mentioned - this may not necessarily indicate true performance. Is there a more specific example/scenario that is exhibited in the application?

And actually, is JS is performing better then Ruby and you are using a series of JS libraries, why are you looking at Ruby? Are the JS libs not performing well for you and you want to look for alternatives in Ruby?

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
If you need to do matrix

If you need to do matrix operations, you might try Ruby Matrix. It's built-in to the Ruby library, but not included in Rhodes. But it appears to be just two pure Ruby files (unless I am missing some C part) and so it's a 5-minute Rhodes Extension!

At least, then, you have some code for common matrix operations that is presumably optimized for Ruby.

    Class: Matrix (Ruby 1.9.2)

I'd make sure to extract it from the same version Ruby used by Rhodes. (1.9.2-p290)

I agree that loops might easily perform poorly in Ruby - especially 1.9.2 - vs Javascript. Were there more "meat" inside the loop, I think you will see better performance. 2.x Ruby VM is much better, but I don't know when/if Rhodes get an update.

I have some experience with this sort of thing... It seems I'm good at identifying and fixing bottlenecks. Some 30 years ago, I wrote a little compiler and interpreter (the interpreter was written in Fortran!) for running simulations of build of mechanical assemblies. Sure, the interpreter was slow as molasses - literally reading opcodes out of Fortran arrays. And loops were slow! But... most of the real work in a real application was done inside of highly-optimized C code. An opcode, in fact, might do an operation on a matrix. Because it was "moving" (virtual) parts together to make an assembly. so, the fact that reading an opcode or performing a branch or loop was slow, it just didn't matter. Nobody wrote their own math routines in my specialized language. They just wanted to write "move fender to body joining plane ..."

I made them hire a real mathematician... I helped him with the C part. He brought the magic heuristics. It wound-up running faster - even with the interpreter - on an IBM PC - than the pure-Fortran code previously ran on a mainframe. That software still exists today (Siemens sells it, and every car manufacturer, etc. uses it).

So, the lesson is... figure out where your code really spends most of it's time, and see wha you can do to optimize that part.  And don't sweat the rest! Sometimes you might do some performance measurements to determine that, but in other cases, it is just intuitively obvious.  I've tried to keep that lesson in mind on many projects since then.

I would give Ruby Matrix a try. It might give better performance than your own code, or at least give you ideas.

Beyond that, if all you need are matrix operations, I would look to port some optimized C library as a Rhodes extension, and then you can use on every platform. Alternately, every platform will have a decent matrix library built-in, and you can expose that as a Rhodes extension, but then you might have some difficulty if the APIs are significantly different on the different platforms.

I don't think that porting a C library for this is very scary or difficult, because it does not involve the native platform. You won't have to account for differences between devices. It's not like it's some device feature - it is just math! Rhodes supports C code in extensions on every platform.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Thierry Leloup
Hi Jon,Thanks a lot for this

Hi Jon,

Thanks a lot for this long but still clear and detailed answer. It helps me to better understand.

Speaking of "Rhodes supports C code in extensions on every platform.", I am not sure I get that. For instance, we have to support FFMPEG library which comes as a C lib but, as far as I understood, we still have to (re)compile it in Java for Android and Objectif-C for iOs, right ? Or is it a way to interface the C version in Rhodes whatever the platform (Android, iOs and Win32 in our case) without having to recompile it to a native version ?

Thierry

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
as far as I understood, we

as far as I understood, we still have to (re)compile it in Java for Android and Objectif-C for iOs, right ?

You mean "port"? Because you can't simply "compile" code in a different language! You would have to re-write it.

Thankfully, you don't have to!

You can use C on every platform. But it can be a round-about way to get there!

If you are writing a native extension API, then the (exposed) interface between Ruby and your native API is, indeed, in the native language for the platform:

  • iOS - objective-C
  • Android - Java
  • Windows C++
  • Windows Phone C#

OK, really, does each platform really have a "native" language? Well, now iOS has two! (Swift). I guess by "native" here, I mean the language used as interface for the platform's own APIs. And... the most popular one. (for iOS).

So, yes, you need to write a bit of objective-C for iOS, Java for Android, etc.

But then that code can call C or C++ code, and they are both supported on at least ios/android/windows (all variants). You get into weird stuff like Symbian (still supported?) or Blackberry, I dunno. Rhodes build is already setup to build those C/C++ files for you.

Browse through the Rhodes source, and you will find that it has plenty of C/C++ under the hood! You build this when you build your project, because you build the entire Rhodes core.

So, you will need to write a small "native" wrapper for FFMPEG for each platform you want to support.

But I would urge you to consider the alternative of using platform-native video processing APIs instead of introducing your own FFMPEG. At least on selected platforms. (e.g. iOS). iPhone/iPad users are heavy video users. Apple has put a lot of effort into audio/video encoding/decoding and signal processing. It all makes use of the GPU.

You certainly do, though, have the option of bringing-in any C library.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
So, you would, somewhat

So, you would, somewhat awkwardly go:

   Ruby -> Objective-C/Java//C++ -> C

And you actually have a C layer first.


In old-style (pre-4.0) native extension (still supported), you used SWIG to create Ruby bindings in C. These went in your extension, and you aren't supposed to modify it. so, really, you go:

Ruby -> C -> Objective-C/Java/C++ -> C

old_extension.png

You could color outside of the lines, though, and just stop at the first step.

So, here, cee_wrap.c is the SWIG-generated Ruby binding. cee.i is the interface specification from which cee_wrap.c was generated. The wrapper acts as the common interface to all of the platform implementations.

With 4.x+, now you write an XML file with the API interface, instead of a .i file. And you don't use SWIG any more, the Rhodes build generates the interface for you. (I don't think it uses SWIG, it uses something else.) I see now there is both C and C++ code in the common part:

new_extension.png

(I just recently converted this native extension from the old style to the new, so it's pretty fresh...)

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Thierry Leloup
OK, thanks Jon for this

OK, thanks Jon for this clarification. It is now much clearer.

A very last question (event though, I think I know the answer...) : is it possible to force a Rhodes application to use a more recent version of Ruby ?

1.9.2 is quite old (August 2010 as indicated here Ruby 1.9.2 is released)..


Thierry

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
You cannot easily change the

You cannot easily change the version of Ruby used internally by Rhodes.

I hope that Zebra will switch to Ruby 2.x some day, but I realize that will be a big job. Rhodes uses compiled Ruby bytecode, and and all of the details of the interpreter and bytecode are completely different now in 2.0.

1.9.2 is modern enough, though, that we do not miss major language features. Much of "the good stuff" came in in 1.9.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Mark Nongkhlaw
https://pragprog.com/book

https://pragprog.com/book/adrpo/ruby-performance-optimization

What d'yall think? Would the contents help to some extent? Would it be relevant to us ' Rho scholars'?

Of course I'm considering buying the book but only if it'll help ... well?

Disclaimer : I dont work for or represent Pragmatic Bookshelf, nah, not in any way.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Jon Tara
So, for example, here is the

So, for example, here is the code for the Ruby Matrix multiply:

    def *(other)

      case other

      when Numeric

        Scalar.new(@value * other)

      when Vector, Matrix

        other.collect{|e| @value * e}

      else

        apply_through_coercion(other, __method__)

      end

    end

You can see it is using collect, rather than looping with a simple Ruby loop. collect() is almost certainly implemented natively in Ruby, using C code. So, there is no Ruby loop at all. That is, it doesn't have the burden of executing bytecode to run the loop.

BTW, I see that Matrix got split into two files in 1.9.3. It's actually just a single file in 1.9.2. This is something where you might get away with using some newer code, since it is just pure math. I'd start with the 1.9.2-p290 version, though.

Vote: 
Vote up!
Vote down!

Points: 0

You voted ‘up’


Log in to post comments