Skip to main content

Sequences: JavaFX Script's Next Challenge?

Posted by opinali on June 18, 2010 at 2:01 PM PDT

I was doing some JavaFX hacking, and I had to create a sequence initially full of zeros. How can you do that? There's apparently only one way:

var bits = for (i in [1..64]) (0 as Long);

Problems: First, I need a loop - OK, a comprehension - to initialize the sequence. There is no syntax, no API helper or type constructor, that directly expresses "Long[] with N elements". I could use a literal like [0, 0, 0, 0, ...], but this doesn't scale to large sizes.

Second, I have to write the (0 as Long), because JavaFX Script doesn't support Java's type suffixes like 0L for zero-as-Long. JavaFX Script drops many complexities from Java; but dropping the numeric type suffixes looks like a wrong move. I mean, it's not like Long numbers are some niche feature. And 0 as Long is butt-ugly.

JavaFX Script should try being as close to Java as reasonably possible, given their different design criteria. I'm OK with big diffs like no generic types (don't fit in FX's complexity budget), different attribute and method/function declarations (required by FX-specific features), and most other changes. But numeric literals is something I'd expect FX to just clone from Java. It's also missing Java 5's hexadecimal FP, and will soon miss Java 7's underscores and binary base (I'd vote to add all these in FX 1.4). These are compile-time features, no cost of any kind. You don't need it, you don't use it -- no impact on APIs, no interaction with other language features. And Java's rich numeric syntax could also be useful in the FXD spec.

Sequence optimizations are not powerful enough here. For the alternative code var bits:Long = for (i in [1..64]) 0, the compiler will create a sequence of type Int[], then in the assignment to bits it will invoke a helper method that converts that to a new Long[] sequence. This could be fixed by new optimizations: in code like xx = for (...) y, where xx is a sequence of x and y needs conversion to x, the compiler could first perform a high-level rewrite to xx = for (...) (y as x), avoiding the cost of allocating a temporary sequence yy only to immediately need a copy-with-conversion to xx. Notice that the per-element conversion (y as x) is often a zero-cost operation, like in our example of Integer->Long

LongArraySequence jfx$177sb = new LongArraySequence();
int i$upper = 64;
for (int i$ind = 1;; i$ind <= 64; i$ind++) {
    int i = i$ind;
    long jfx$178tmp = 0L;
    jfx$177sb.add(0L);
}
$bits2 = (Sequence)Sequences.incrementSharing(jfx$177sb);

The decompiled code above shows that there are no optimizations for initial capacity. The internal LongArraySequence class contains the necessary constructors that take an initial size as argument; but this is probably only used internally, by runtime code written in Java - the javafxc compiler has no intelligence yet to use it.

There are two ways to fix the performance of this code:

1) (The Wrong Way) Adding special syntax to allocate a sequence of fixed size, e.g. new Long[64] or just Long[64].

This is the "wrong way" because we're thinking in Java, not in JavaFX Script. First, generator syntax is already good and terse enough. Second, the proposed syntax makes more sense to work with mutable sequences, and this is not the paradigm that JavaFX Script's sequences are pushing. Sequences are immutable (more exactly "persistent", to use a Clojure term); only sequence variables are mutable, each mutation creates a new sequence. This is expensive even though javafxc does some optimizations to minimize the churn.

Having said that, sometimes we need mutable sequence variables, and the costs can be low if we program carefully, and if the compiler does its part - that's what I am missing here. And there isn't really a better way (except "impure" ways, like using nativearrays or Java classes).

2) Adding the necessary optimizations, so a for that produces a sequence filled with a single value will do it as fast as possible.

Or, just make the for generator efficient. First, preallocate the sequence whenever the size can be detected. Second, bulk-fill the sequence [slice] by invoking Arrays.fill().

The latter optimization is very interesting to discuss:

- It's only valid if the body of the for loop is either a compile-time constant, or a functional-pure expression that also doesn't depend on variables changed inside the loop. The javafxc compiler already does simple pureness detection (for 1.3+'s binding), and I hope that will keep improving because this makes many new optimizations possible and even easy.

- Arrays.fill() is JavaSE-only, so this optimization wouldn't be supported when compiling with -profile mobile. But this can be worked around with some runtime-lib stubs.

Without these enhancements, programmers are temped to drop to native arrays (ugly), and invoke Arrays.fill() manually (non-portable). We can imagine a new API with methods to fill a sequence [slice] with a single value, and other bulk-ops like copy. But this is not the "JavaFX Way". Like said before, you are supposed to use for-comprehensions. It's certainly cleaner than something like "bits = new Long[size]; Sequences.fill(bits, 0, sizeof bits, value)" - yuck!!

Yes there's already a javafx.util.Sequences API with functions like sort() and binarySearch(), but these are complex enough to deserve APIs... at least in the current language. Simpler things like Sequences.max() would disappear in a language with some extra functional programming tricks, e.g. max = reduce(seq, >, Long.MIN_VALUE). [You can do that today, but not with enough clean code or enough performance.]

javafxc's next steps?

For the JavaFX Script Compiler project, 1.3 was the Compiled Bind Release, and 1.3.1 is the Debugging Release. 1.3.2 will apparently be mostly a maintenance release fixing low-priority JDI and binding bugs, and a few optimizations - at least JFXC-4388: Iterating over sequences created by bound for loops is very slow is planned and very important (this actually seems to be a closure optimization, not a sequence optimization). Next feature release, 1.4 (Presidio), will address many binding optimizations that slipped the 1.3 deadline - remarkably code-bloat fixes to reduce the speed X size tradeoff from the initial Compiled Bind. I see plenty of sequence-related fixes too, but not (yet) significant sequence optimizations.

There is a bug JFXC-1964: Umbrella: Sequence optimizations, but this bug (now in "After-Soma" limbo) was from the JavaFX 1.0-1.2 releases. It enrolls 27 bugs, 24 of which are fixed. Most of the described optimizations seem to be "fundamental" things (e.g. flattening optimizations), or low-handing fruit things like looping with straight indexing instead of Iterators. There's no [visible] plans, yet, to enable a good set of higher-level sequence optimizations. Given the importance of sequences in JavaFX Script, I hope this won't take too long :-) it seems to me to be the next logical step to enhance the implementation of the current language. There are other open avenues, like closure optimizations (but I guess these will have to wait for JDK 7... JavaFX will eventually have to sync with Java's lambdas/closures, both for interop sake and also to benefit from new VM support).

A high-level language like JavaFX gives the source translator vast opportunity to enhance code performance, without imposing any cost to the runtime (library size, warm-up time, memory usage or anything). This is very different from the tradition of the Java language, which syntax is very close to the "machine", so javac is purposefully a non-optimizing translator: the responsibility for optimization is fully in the shoulders of the runtime. A design that doesn't always win, because new language features are often introduced without sufficient new support from the bytecode/VM. Or because the weight on the runtime's shoulders has long become an important problem - remarkably for client-side or mobile apps, needing fast startup and low resource usage.

Before Java, I was a C++ programmer and I appreciated that the compiler would do a massive optimization effort. Long build times could be avoided with a mountain of hacking (precompiled headers, incremental linking...) and even if they are big, that's a good tradeoff for the very best application performance. Java changed this upside-down by moving all optimization to the runtime; and this has advantages like portability, dynamic features, and dynamic optimizations that often beat C/C++. But Java's move was maybe too radical. Over the last few years and releases, we've been slowly compensating this with some efforts to move overhead to the compile- or installation-time: the Class Data Sharing (CDS); the bytecode preverification (created for J2ME and adopted by JavaSE 1.6); the JIT caching of some JVMs; hybrid AOT+JIT VMs like JET. New languages like Scala, JavaFX Script, Clojure and JRuby seem to be yet another evolutionary step, as they offload more optimization responsibility back to the source compiler. JavaFX is a layer atop Java and it can't fix problems like long warm-up and poor memory sharing of JVM processes; but it can (and must) not add any extra runtime load.