1

Many programming languages have an implementation language (mostly a system language like C) which is used to implement important parts of the core of the language. Besides the libraries are written in the language itself (like Ruby or Python). My question is for an interpreted language like Ruby or Python, this means that each time I run a program (by say python3 my_program.py which makes use of the language libraries which are written exclusively in the language itself, those libraries (classes and functions etc.) are first handled by C? This sounds a bit inefficient. Any helps to understand how those languages handle this is appreciated.

3
  • The library code is not "handled by C" any more than the code in your own program, it is interpreted. The interpreter may itself have been compiled by a C compiler, but that happened long before, and only once. Commented Sep 24, 2021 at 7:09
  • Ok, but that interpretation of the core library utilities happens upon each call to say python my_program.py?
    – Student
    Commented Sep 24, 2021 at 7:17
  • In principle, yes, every time a line of Ruby/Python/Qbasic... code is executed, the interpreter does some work interpreting it. However, modern interpreters can byte-compile/JIT-process things that will make your head spin, often to the point where interpretion overhead becomes negligible or even negative. (The JVM in particular is notorious for often performing better than naively compiled static code, because it can observe and adapt to runtime code statistics.) Commented Sep 24, 2021 at 8:09

2 Answers 2

6

Correct! With a lot of interpreters, most libraries – and even large parts of the standard library – are written in the interpreted language itself. That is inefficient, but this inefficiency might not matter. E.g. web applications are typically slow regardless of programming language used, because other effects (like network latency or database queries) dominate. Also, languages like Python and Ruby tend to value productivity and simplicity over performance.

But there are ways to get better performance.

  • Often, these interpreters make it possible to write libraries with native code that can run without the interpreter. For example, the Python Numpy library does include some Python code, but it just delegates the main work to ultra-optimized native code written in C and other low-level languages. When dealing with large amounts of data, a Python+Numpy program could even end up being faster than a typical C program written for the same purpose (though the C program could just end up using the same low-level libraries as well). Performance-critical parts of the standard library are typically integrated with the native code of the interpreter.

  • It is possible to compile high-level programming languages to machine code, at runtime (Just in Time Compilation, JIT). The machine code can then run like a native C program, without the overhead of the interpreter. For example, this JIT approach is typically used by Java. Ruby is increasingly using JIT to become faster. Python has alternative implementations like PyPy that rely on JIT, but even in the default CPython implementation you can use Numba to JIT-compile individual functions.

3

Many programming languages have an implementation language (mostly a system language like C) which is used to implement important parts of the core of the language.

This is not true. The vast majority of programming languages do not have an implementation language. For example, Ruby, Python, PHP, ECMAScript, Java, C#, Visual Basic.NET, Kotlin, Dart, Hack, etc. all do not have an implementation language.

A specific version of a specific implementation of a programming language may or may not have a single implementation language. For example, The GHC Haskell Compiler is mostly written in Haskell, with a small C runtime, and (at least in some older versions), some Perl scripts for post-processing the generated assembly code before it gets handed off to the system assembler. So, GHC does not have one implementation language but three.

Or, for example, CPython is written (mostly) in C, Jython and GraalPython are written mostly in Java, IronPython is written mostly in C#, and PyPy is written mostly in RPython. YARV is written mostly in C, Rubinius is written mostly in Ruby and C++, JRuby and TruffleRuby are written mostly in Java, IronRuby is written mostly in C#, Artichoke is written mostly in Rust, Opal is written mostly in Ruby and ECMAScript, MagLev is written mostly in Smalltalk. Then, what is the implementation language of Ruby?

The implementation language depends on … well … the implementation. Sometimes, it even depends on the version on the implementation, e.g. early versions of Deno were written in Go, but current versions are written in Rust.

Besides the libraries are written in the language itself (like Ruby or Python).

This depends on the implementation. For example, if you just "Install Ruby", you will most likely install the YARV implementation of Ruby. In YARV, the entire core library, and large parts of the standard library are written in C. On the other hand, in Rubinius, large parts of the standard and core libraries (including important, foundational classes like Hash and Array) are written in Ruby. And many other implementations have used the Rubinius libraries for their own, for example, Opal's standard library is based on Rubinius's, with some ECMAScript parts.

My question is for an interpreted language

There is no such thing as an "interpreted language". A programming language is not interpreted or compiled. A programming language just is. A programming language is an abstract set of mathematical rules and restrictions. These rules specify what specific programs mean, but they almost never specify how a program is supposed to be executed.

Interpretation and compilation are properties of the, well, interpreter or compiler (duh!) not the programming language.

Every language can be implemented by an interpreter and every language can be implemented by a compiler.

Those two terms ("interpreted" and "language") belong to two completely different levels of abstraction. If English were a typed language, the term "interpreted language" would be a Type Error. The term "interpreted language" is not even wrong, it is non-sensical.

In fact, most programming languages have both compiled and interpreted implementations. For example, there are interpreters for C and C++. Does that make C and C++ interpreted languages? And every single mainstream implementation of Ruby, Python, and ECMAScript that is currently in widespread production use actually has a compiler – some even have more than one.

Just a few examples:

  • Opal is purely compiled. It never interprets. There is no interpreter in Opal, only a compiler.
  • YARV compiles Ruby to YARV byte code. This byte code then gets interpreted by the YARV VM. Code that has been executed more than a certain number of times then gets compiled to native machine code for the underlying architecture (i.e. when running the AMD64 version of YARV, it gets compiled to AMD64 machine code, when running the ARM version, it gets compiled to ARM machine code, and so on).
  • Artichoke is … somewhat complicated, but suffice to say, it does not interpret Ruby.
  • MRuby compiles Ruby to MRuby byte code. This byte code then gets interpreted by the MRuby VM.
  • Rubinius compiles Ruby to Rubinius byte code. This byte code then gets interpreted by the Rubinius VM. Code that has been executed more than a certain number of times then gets compiled to native machine code for the underlying architecture (i.e. when running the AMD64 version of YARV, it gets compiled to AMD64 machine code, when running the ARM version, it gets compiled to ARM machine code, and so on). [Note: there are a couple of different versions of Rubinius. The original version had a native code compiler. This was then removed, and is in the process of being rewritten.]
  • JRuby compiles Ruby to JRuby IR. This IR then gets interpreted by the JRuby IR interpreter. Code that has been executed more than a certain number of times then gets compiled to JRuby compiler IR. This compiler IR then gets further compiled to JVM byte code. What happens to this JVM byte code depends on the JVM. On the HotSpot JVM, the JVM byte code will be interpreted by the HotSpot interpreter, which will profile the code, and then compile the code that is executed often to native machine code.
  • TruffleRuby parses Ruby to Truffle AST. This AST then gets interpreted by the Truffle AST interpreter framework. The Truffle AST interpreter framework will then specialize the AST nodes while it is interpreting them, including possibly compiling them to native machine code using Graal.
  • GraalPython parses Python to Truffle AST. This AST then gets interpreted by the Truffle AST interpreter framework. The Truffle AST interpreter framework will then specialize the AST nodes while it is interpreting them, including possibly compiling them to native machine code using Graal.
  • CPython compiles Python to CPython byte code. This byte code then gets interpreted by the CPython VM.
  • SpiderMonkey compiles ECMAScript to SpiderMonkey byte code. Code that has been executed more than a certain number of times then gets compiled to native machine code for the underlying architecture (i.e. when running the AMD64 version of SpiderMonkey, it gets compiled to AMD64 machine code, when running the ARM version, it gets compiled to ARM machine code, and so on).
  • SquirrelFish Extreme compiles ECMAScript to SquirrelFish Extreme byte code. Code that has been executed more than a certain number of times then gets compiled to native machine code for the underlying architecture (i.e. when running the AMD64 version of SquirrelFish Extreme, it gets compiled to AMD64 machine code, when running the ARM version, it gets compiled to ARM machine code, and so on).

As you can see, there are plenty of implementations of Ruby, Python, ECMAScript, and co. (including all current mainstream implementations) that have at least one, if not multiple, compilers.

this means that each time I run a program (by say python3 my_program.py

What happens then depends highly on which implementation you are using. CPython, PyPy, Jython, IronPython, they all work very different from each other.

which makes use of the language libraries which are written exclusively in the language itself,

In the most popular implementations of those languages you listed, that is actually not true. For example, in CPython, which is the most widely-used implementation of Python, a significant portion of the libraries are implemented in C. In Jython, a significant portion is implemented in Java, in IronPython, a significant portion is implemented in C#. In YARV, which is the most widely-used implementation of Ruby, a significant portion of the libraries are implemented in C, the same applies to MRuby, as well as to the previously most widely-used implementation, MRI. In Ruby, a significant portion of the libraries is written in Java, in IronRuby in C#. Some newer implementations reuse portions of Rubinius's libraries, which are indeed written in Ruby, though. In SpiderMonkey and SquirrelFish Extreme, two of the most widely-used implementations of ECMAScript, practically none of the libraries are written in ECMAScript, it is pretty much all C++. Only V8 has ate least some portions written in ECMAScript.

those libraries (classes and functions etc.) are first handled by C?

It is unclear what you mean by "handled by C". C is a programming language, it is basically a piece of paper with rules written on it. It doesn't "handle" anything per se.

This sounds a bit inefficient.

Having the libraries written in the same language as the user programs is actually pretty efficient. After all, a Ruby implementation's job is to make Ruby code run really fast. If the libraries are not written in Ruby, then it can't make them run really fast, since it doesn't understand their code.

If, on the other hand, the libraries are written in Ruby, then the Ruby implementation understands the libraries and can make them run really fast. Even more, since it understands both the libraries and the user program, it can perform optimizations across the boundary between libraries and user programs, for example, it can inline portions of the libraries into the user programs, when the user program calls a library subroutine. And vice versa, when a user program passes a callback to a library subroutine (for example a comparison function into a sorting subroutine), then the callback can be inlined into the library subroutine.

The same, by the way, applies to the runtime system: if the runtime system is written in the same language as the libraries and the user program, then these same optimizations can be applied across that boundary, too. For example, the Maxine VM is a meta-circular JVM written in Java, that runs inside of itself. Its compiler can even inline parts of the memory allocator or the method dispatcher into the user program, or can inline finalizers into the garbage collector, etc.

As a general rule, a language implementation can only optimize code that it can see and understand, so the more code is written in the language itself, the more code it has that it can optimize. Conversely, if significant portions are written in a different language (e.g. in YARV, where the compiler, VM, interpreter, JIT compiler, garbage collector, memory allocator, method dispatcher, all the core classes, significant portions of the standard library, etc. are all written in C), then it cannot optimize those parts.

This is where polyglot execution engines like Truffle/Graal come in. Truffle contains an interpreter for Ruby, Python, ECMAScript, R, Lua, JVM bytecode, and even one for LLVM bitcode! This means it can optimize across all those languages.

For example, MRI was very slow, and YARV used to be pretty slow as well. (And still is, compared to some high-performance implementations like TruffleRuby, V8, or HotSpot JVM.) Therefore, when Ruby programmers needed performance, they would write a C extension.

There are two problems with this, though: as Ruby implementations improve, the Ruby code gets faster, but the C code doesn't. And some Ruby implementations cannot run C extensions at all, or only run them slowly.

For example, JRuby is significantly faster than YARV, so you might be inclined to switch to JRuby in order to make your code faster. But, if you are using a library whose author wrote a part of it as a C extension to make it "run faster", it will ironically run slower on JRuby: while Ruby can run (some) C extensions, crossing the boundary between the JVM and C is slow as hell, because for each call and each return, the JVM does a number of checks to make sure its safety properties are not violated.

In TruffleRuby, however, the "C extension" will actually run interpreted on top of the JVM, which means that Truffle can optimize the Ruby part and the C part together. The developers of TruffleRuby have demonstrated an example, where TruffleRuby was running Ruby code that heavily relied on a YARV C extension faster by interpreting the C extension that YARV did with a natively compiled version of that same C extension.

4

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.