Is it wrong to call Julia interpreted?

Matlab was only interpreted; also Python I believe at some point way in the past. By now Matlab is JIT compiled to native code. I like the term just-ahead-of-time (JAOT) compiled for Julia, which happens per method. Python is actually also compiled by now (also JAOT(?); I’m not sure, or per file?), but not to machine code, but to its own bytecode. Unlike Java where you distribute compiled (Java) bytecode, I believe that’s not done for Python(?).

Python is much slower than Julia for two reasons, because even if strictly speaking compiled (but not thought of that way, since you don’t distribute binary executables by default, or bytecode), the bytecode is still interpreted. Not with Matlab since it compiles all the way. The other reason Python is slow is it’s too dynamic, and hard to optimize for, and it’s not even tried much. Matlab is also I believe too dynamic (though not as?), and well all dynamic languages before Julia. You can also fully compile (a restricted subset of Python) to machine code, with e.g. Numba.

Being compiled to bytecode will give some speedup, when it’s then interpreted.

Compiling to machine code, JIT or otherwise will be much faster, but there are also levels to compiling. Compiling to naive machine code is much better than not compiling. Then on top you can compile to optimized code, i.e. for Julia/C/C++ -O0 (strictly speaking not no optimization for Julia unlike for most languages), -O1, -O2 (Julia’s default), -O3.

even if not fully interpreted it could by like early Java (which compiled to bytecode, before it introduced JIT), still interpreting but now a bytecode, same as Python currently. It could also mean JIT compiled to native code, like Julia, I’m not sure.

The terms interpreted and compiled are a bit blurred by now by JIT or JAOT. For Java JIT applies to bytecode. It can also optionally by compiled to binary executables.

3 Likes

As @Palli says I think that’s mostly historical. If Matlab had first appeared in its current form probably it would not be classified as interpreted.

I think it’s quite simple really: if the language runtime interprets your code every time it executes (as Python does with the bytecode) then it’s interpreted. If it generates a machine language version that can be run by the CPU without having the runtime looking each time at every instruction, then it’s not interpreted.

Things only get blurred when speaking of compilation, which refers to many things, and people are confused because historically the interpreted languages didn’t do much compilation if at all (but now they do). (Edit: and by the fact that the same language and runtime can have different modes of executions, and several modes can be active at the same time. OK it’s not that simple then…).

The point is that originally, interpreted only meant that you evaluate the code directly.
No intermediate step. And yes, old Python versions were still interpreted.

The term comes from a time, where languages compiled to machine code, or were interpreted.
So basically C and Bash, later Perl and so on.

Languages like Lisp, Smalltalk, and later Java blurred the lines.

But the people who wrote C, C++ and similar, considered these VM based languages as interpreted primarily because people like to divide the world into a simple black and white. :wink:

VM based languages - like the JVM and .NET based ones - actually translate their code into machine code these days, but some people still call these languages interpreted.

Historically, and strictly speaking, are interpreted languages who are directly evaluated.

And as said, the last popular interpreted general programming language was Ruby 1.8.
Otherwise, you could still argue, that the computer is “interpreting” machine code as well.

I love the term “just ahead of time.:smile:

1 Like

There’s a spectrum.

  1. Pure Interpreter: parses the code and stores the abstract syntax tree (AST). When executed a program called the “interpreter” carries out the tasks implied by the AST in terms of modifying the state of the machine in whatever way. (Example the Bash shell)

  2. Bytecode Interpreter: parses the code, takes the AST and produces a sequence of byte-codes for an abstract machine, when executed a program called an interpreter carries out the tasks implied by the bytecode. (example CPython, prolog with WAM, erlang + BEAM, early Java)

  3. JIT compiled: parses the code, takes the AST and (usually) generates a bytecode sequence, then after some number of executions with a bytecode interpreter (possibly zero) converts the bytecode sequence to machine code and execution from then on is carried out by telling the CPU to jump to the machine code instructions. (example Java, maybe things like C#, a lot of modern javascript)

  4. Just Ahead of Time Compiled: parses the code, takes the AST, generates an intermediate IR representation, at first execution, it passes the IR to an optimizing IR to machine code compiler, stores the machine code, and the first time and every time after executes the machine code by having the CPU jump to the machine code instructions. (Julia, SBCL)

  5. Ahead of Time Compiled: parses the code, takes the AST and usually generates an intermediate representation, then passes the representation to an optimizing machine code compiler, writes out a file with all of the generated machine code creating an “executable binary” which carries out ONLY the one task. Usually no further compilation occurs during runtime. (C, C++, Rust)

So the “interpreter vs compiler” is orthogonal to “interactive vs non-interactive” in that basically everything but 5 can be interactive. If you collapse 1-4 into “not AOT compiled” and call that “interpreted” it’s a controversial statement because for different people interpreted may mean 1 or both 1 & 2, most people don’t consider 3,4 as interpreted.

17 Likes

Yes, if the interpreter “looks” “each time at every instruction”. It’s doesn’t have to be, as you explain, strictly speaking your [source] code.

At some point in the past I think interpreter looked at least instruction, but not only that, each letter of (i.e. did parsing/lexing too), likely e.g. with Lisp (probably still if interpreted), and Bas I would guess. Then with at least most BASICs you got tokenization (but earlier/first BASIC was actually a compile-and-go system).

Bytecode is now common (and more or less the same as tokenization in BASIC, though I’m not sure). [Julia actually has also LLVM’s bitcode, but that’s an implementation detail, and I’m not sure it’s a bytecode, despite the similar name, it’s meant as part of the compilation backend, not sure if it helpful or meant too to be interpreted. I would like to know.]

Interpreters can have very different speeds, and if it means to you slow, then that needs not be the case. If you compile to low-level code, similar to machine code then slow.

You can compile to rather high-level bytecode, the sky is the limit, and your imagination. E.g. you can plausibly have a single bytecode instruction to multiply matrices, and it wouldn’t be slower than what Julia does (assuming non-small matrices). I find it likely that Matlab has such, but doubt Python, since it didn’t have an operator for until it added @ for it recently. Nothing rules out e.g. a bytecode instruction for quicksort either. My interpreter would be faster than some (naively) compiled code, even if they used the same algorithms.

So what is one instruction to you? E.g. you have instructions in comprehensions, but can it as a whole be one?

I haven’t looked closely at this new in Python 3.12:

This change makes comprehensions much faster: up to 2x faster for a microbenchmark of a comprehension alone, translating to an 11% speedup for one sample benchmark derived from real-world code that makes heavy use of comprehensions in the context of doing actual work.

See there for gory details, their bytecode (for this), there, some have no assemly equvalent, I see e.g. MAKE_FUNCTION and LOAD_FAST.

This BOLT is new to me, I don’t think Julia has it yet, but Python added in version 3.12 for up to 17% faster (4% is the geometric mean of their benchmarks):

See: https://github.com/llvm/llvm-project/blob/main/bolt/README.md

BOLT is a post-link optimizer developed to speed up large applications. It achieves the improvements by optimizing application’s code layout based on execution profile gathered by sampling profiler, such as Linux perf tool. An overview of the ideas implemented in BOLT along with a discussion of its potential and current results is available in CGO’19 paper.

It’s on my TODO list to make my own bytecode (for Julia, and myself to experiment). They can be quite fast, in my design you could loop without any memory access, and in phase 2 I would make it support many threads of execution…

2 Likes

I like this - Laurence Tratt: Distinguishing an Interpreter from a Compiler

3 Likes

Seems problematic. The time it takes to execute an expression in Julia is composed of two components… one is compilation time, and the other is execution time. So the time it takes heuristic is not helpful. He’s really talking about just the compilation step in an ahead of time compilation.

Ack I see I spoke too soon and if I scroll down he addresses this issue by splitting out the time components.

So, having made it to the bottom, I think the thing that is missing is, like he said, the constant multiplier. CPython is a bytecode compiler, so it has the same big O complexity, but it’s a factor of 100x slower or something. Most people still call this a “bytecode interpreter” I think because anything that isn’t just jumping to some machine code is considered “an interpreter”.

2 Likes

If you collapse 1-4 into “not AOT compiled” and call that “interpreted” it’s a controversial statement because for different people interpreted may mean 1 or both 1 & 2, most people don’t consider 3,4 as interpreted.

I feel like it’s really not that controversial if you say it compiles at runtime in the next sentence. People sometimes use the word “interpreted” in the context of JIT compilation when you’re handing source files to a program that executes them, and apparently I’m one of those people. Pypy, for example, is sometimes referred to as a Python interpreter in its own documentation.

1 Like

Actually, they officially call it a bytecode “virtual machine”. The main difference is nobody in the Python community gets irritated if you call it a bytecode interpreter because it all means the same thing. It handles bytecode more or less the same way as the JVM does, the main difference being that the most popular JVM, HotSpot, has a tracing JIT.

I agree, and for me it basically boils down to:
if you can call a function without it already being compiled, it is interpreted. Even if it gets compiled around the time of the function call.

2 Likes

This defintion does not form a disjoint nor complete cover of all things.
That’s fine, it just means that some things are neither and some thing are both.

Under that definition here is my C interpretter. I wrote it myself.
It runs code from source.
alias gci='gcc -xc - && ./a.out'

Look:

$ echo "                      
    #include <stdio.h>    
    int main(){    
    printf(\"Hello C Language\");    
    return 0;   
    }  
" | gci

outputs:

Hello C Language

So that is both an interpretter and a compiler.

And honestly my gci program is not even that different to Julia in how it works.
It compiles it just ahead of time to run it.

If that’s the defintion you want to use then … well its a free world.
But generally language is used to communicate with others.
And so you want a description that will help other people know what you mean.

8 Likes

My intention by using the word interpreted is to let people know that you can use Julia interactively and that you run a program with julia my-great-code.jl (or by adding the appropriate hashbang to the file). I don’t think JIT-compiled conveys this adequately because it may be associated by some with the workflows presented by languages like Java or C#. Java can technically even be called “interactive” these days because jshell is distributed with the JRE, but it’s still a different workflow.

If I say “interpreter with JIT compiler”, people will understand “ah, it’s probably something like Node, then”, which gives a more or less accurate impression. Of course, one may further qualify that Julia’s JIT uses a very different compilation strategy internally, which some fondly refer to as JAOT, which tends to produce more efficient machine code because it doesn’t need as many runtime checks.

Of course, since the purpose of language is to communicate, you can’t really say “JAOT” without explaining, since this is not a concept that most people will be familiar with.

I feel like there is some sort of fear that people are going to assume Julia is slow if I use the “wrong” word. I really believe this fear is unfounded. You can’t do even the most cursory search for the language without seeing people rave about the performance.

For me, the “miracle” of Julia is that it has the runtime performance characteristics it does while still presenting a programming paradigm and workflow similar to Python, Matlab, et al. I consider that quite brag-worthy. The big story is not that Julia is so different from these languages, but that it is so similar while still subverting all expectations about how they are supposed to perform. This is what I wish to communicate.

If one wishes to explain how this is accomplished on a technical level, it requires some discussion in any case, since no single word encapsulates all that Julia does in terms of type inference and runtime code specialization.

1 Like

you know you audience better than I do.
I would prefer to call it interactive then.

6 Likes

Here is a stab at defining compiled and interpreted languages:

  • An interpreted language is one which has the ability or need to run source dynamically, i.e. has parsing and evaluation available during run-time.
  • A compiled language is one which can run without a need to resort to parsing and translating code from source, i.e. does not have to use Meta.parse and Base.eval at run-time.

Given these definitions, Julia is both an interpreted and a compiled language (although creating a binary without the parse/eval code is not standard AFAIK).

This is a stab at the question, and stab is used because these nitpicky definitional discussions are somewhat painful.

1 Like

The term for that is separately compiled, or so I believe (I guess at some point compiled was just used for this; note also original Dartmouth BASIC was a compile-and-go system, i.e. a compiler, sort of like Julia without a REPL?). I often try to be clear by stating C, C++ etc. are such; and that Julia is optionally separately compilable.

1 Like

I think the interesting (and frankly weird) thing about Java and similar languages is that you compile them to one thing ahead of time, and then the runtime compiles it to something else later.

But I guess this also parallels what happens with machine code—frequently being the “ultimate” compiler target, and then actually being converted to microcode for execution—at least on many popular architectures.

It’s pretty crazy to think about all the transformations our code goes through before it actually hits CPU gates. For Julia, it’s something like:

  • In Julia
    • source
    • macro expansion
    • several lowering and inference representations
    • LLVM IR
  • in LLVM
    • is there a bytecode phase? I don’t even know. I think there is at least some lowering and optimization done on the IR.
    • assembly
    • machine instructions
  • in the CPU
    • converted to microcode

It’s quite a journey the code goes on!

1 Like

credit @vchuravy julia-parallelism

9 Likes

It’s not so weird, it’s for these goals:

  1. platform independence. It wasn’t the norm (for at least fast languages) when Java first came out. There were similar p-code and ANDF before.
  2. for hiding the code. Important to many. Julia is also platform dependent (for at least all important desktop OSes), and Python, back then not popular, and Perl, bash… but all of those by showing the code, until Julia could hide it.
  3. [non-goal? Java is a language, Java VM is something else, a different concept, strictly speaking also a different lower-level language.] The point wasn’t maybe from the start to allow other competing languages to compile to it, allowing competitors like Kotlin and Clojure, but that’s what happened.

WebAssembly is the spiritual successor to Java VM (and .CLR of .NET). You compile to it, it’s an instruction set, but not machine code, i.e. for a real machine, rather a virtual machine. Some hardware was made for Java VM, so this distinction isn’t too important, I haven’t thought it through, maybe we could see WebAssembly hardware too.

WebAssembly is then meant to be compiled again to machine code. I think you can also interpret it (you can compiler or interpret all languages, I mean was it practically meant to be interpreted?). Was that ever the goal or done? As opposed to Java VM bytecode that was only interpreted for some year until JIT. Was JIT always a future goal there?

[You could always say machine code is interpreted… by the CPU. That’s sort of true, while to most interpreted means slow and in-order/non-parallel. CPUs (even each core) are parallel creatures with superscaler usually, though some simpler cores are in-order. That used to be the norm, now you have such efficiency cores, and other high-performance, those are superscalar with register renaming.]

2 Likes

Yes, I always forget that some people want this security blanket of distributing obfuscated code. This will never stop a determined person from figuring out what you’re doing, but I guess it at least makes it harder and that makes some people feel good.

I just never think about this because everything I write is either open source or runs on a server (or both).

1 Like

I left an example of using the interactive codegen interogation tools in the Github issue:

2 Likes