Hacker News new | ask | show | jobs
by gary4gar 4657 days ago
Curious to know what are thing in "ruby proper" that make it slow?
2 comments

The short answer is that Ruby is "too dynamic" and "too malleable": An object can get a new class when you call a method on it, for example, or every method might be redefined, including seemingly safe ones like arithmetic on constant integers.

The longer explanation of this one issue (there are others):

Somewhere deep in the bowels of your code, some library calls eval(foo). "foo" is what exactly? If you can't track it back to a constant string, you're SOL from an optimisation standpoint: Any and all method pointers and class pointers you may have cached to speed up method calls are now unsafe, as for what you know, the next time you try to do "2 + 2" you might get "5". Or a string. Or your harddrive might get formatted - that latter is particularly important: Methods may change from not having side effects to having side effects or back, potentially in the middle of evaluating an expression.

99% of the time, the changes will not affect core classes, but even so a language implementation that wants to be complete will need to at least guard against it even for basic things like adding two numbers.

That means there's lots of stuff you can't cache, or where you need logic to invalidate caches that can be very complex, or you need to be able to validate any cached information. And you either need to do this for every method call, or you need to find ways of safely rolling back calculations (and you'd need to ensure you're not inadvertently triggering side effects that you can't roll back if you choose that option), or you need to be able to deduce what code can trigger these things, and tread with appropriate caution afterwards.

For example method cache invalidation is a hot topic in dynamic languages that is often ignored in synthetic benchmarks. A Ruby example for this is:

  output.extend(ContentTyped)
(thats from Sinatra: https://github.com/sinatra/sinatra/blob/154859f1553b8bacea97...)

I am not saying that these things cannot be implemented fast, but are usually the things that get ignored in a first implementation and take a lot of work to get right and quick.

Now I want to slap a Sinatra developer.

Note that the problem is not to implement the example you gave fast. That example almost certainly won't be: Implementing it without instantiating a new eigenclass just for that object and populating it with the right methods would be tricky, and that is not going to be particularly fast.

The problem is as you mention that method caches must be invalidated, affecting other calls that we otherwise would expect to be fast, and requiring overhead everywhere to prevent doing the wrong thing in the face of having the rug pulled from under you.

In this case it's fairly benign: ContentTyped "just" installs attribute accessors, and "only" affects that object, and sufficient bookkeeping could restrict the cache invalidation accordingly.

(Thanks, btw., it's an interestingly rare example of a real use of extending objects directly)

The problem here is not just that it trashes the method cache for this object. In most versions of MRI, this trashes _all_ method caches, not just the ones for String.

A sufficiently clever JIT could find out that there is only ever a String instance extended (so, basically, the extension is monomorph) and introduce the resulting type.

However, this example is not rare. Some more:

The DCI pattern: http://www.sitepoint.com/dci-the-evolution-of-the-object-ori... (scroll to source code) ROAR, a popular representation gem: https://github.com/apotonick/roar

It shouldn't need to cache method caches for String at all. It is not extending String. It is extending a specific instance of String (or whichever other class that object happens to be - for the rest of this I'm assuming it's a String), and thus not even touching the String class, but the eigenclass of the object:

  >> module ContentTyped
  >> attr_accessor :content_type
  >> end
  => nil
  >> foo = "bar"
  => "bar"
  >> foo.extend(ContentTyped)
  => "bar"
  >> foo.content_type
  => nil
  >> "another string".content_type
  NoMethodError: undefined method `content_type' for "another string":String
  	from (irb):31
Externally this eigenclass is not readily visible, but within MRI it is, and for the purposes of a method call, the immediate superclass of foo above is not String, but its own object-local class object.

If you are right this might be one of those areas where there's room for quick-wins. It doesn't take a "clever JIT" to localise this damage by ensuring the method cache is only invalidated for whichever entity is extended (which in this case is no existing class, unless the object has been previously extended).

As for your examples, I wonder if these people realize that their code is equivalent to instantiating a new class for every single object. To me ROAR for example just looks horribly conceptually broken. In effect they are creating a new class per object in order to add non-stateful behaviour, which just makes me want to rage

(EDIT: and DCI makes me want to rage too, for different reasons - they could achieve most of the same by composition by wrapping the objects in static classes instead of dynamically extending the objects; to me these examples stands as prime examples of how the horrible performance of MRI leads to people making implementation choices they'd never make with a Ruby implementation where the things that can be made fast are fast).

I don't care if that is never a fast case, as they have options that would be faster: include the representer modules in the resource class like Article, subclass and include, wrap it when needed. I want it to be easy to write fast Ruby code - I don't particularly care if pathologically crazy implementation choices remain slow...

> It is not extending String. It is extending a specific instance of String, and thus not even touching the String class, but the eigenclass of the object:

Whoops!

  module ContentTyped
    def self.inherited(base)
      String.instance_eval do
        def lol
          "lol"
        end
      end
    end
  end