Use primitive arrays in place of Array<T>

Is there a way for a parent interface to ask for an Array to be overriden by a child with primitive arrays, like FloatArray instead of Array?

No, “primitive” arrays are not integrated into Kotlin type system. You can do a dynamic dispatch like

when(input){
  is DoubleArray -> ...
  is IntArray -> ...
  else -> ...
}

But you can’t enforce those types since they are stand alone and not a part of hierarchi. You can wrap them in a class though like it is done here: kmath/DoubleBuffer.kt at 2144c6382cce2eae27c4cd55bab693cfeda7e54d · mipt-npm/kmath · GitHub

2 Likes

Man why can’t FloatArray extend Array by default ;-;

In my opinion it is kind of design flaw. The problem is that in JVM arrays are a different kind of object and not classes. Integrating arrays into inheritance system while keeping java interop would require a significant effort from the team at that time and they decided not to do it. Too late now since any change would break backward compatibility. Maybe in Kotlin 2.0…

2 Likes

Any specific reason why you use arrays and not lists? Arrays are pretty low-level in Java, they aren’t very flexible from the type system perspective and therefore they have similar limitations in Kotlin.

Also, you can always cast between Array<Any> and Array<Float> if this is what you need. It won’t be as memory-packed as FloatArray though.

And if you mean that you want to have e.g. val arr: Array<Any> in the interface and then override it with override val arr: FloatArray then no matter the limitations of arrays, this shouldn’t be allowed as this is not type-safe.

3 Likes

Arrays are needed for performance optimization and interop. One should not use them otherwise. But those to cases are a pain.

1 Like

Two further reasons: vararg, and params to the main() method.

But yes, arrays are second-class citizens in Kotlin; lists are much better supported in the standard library, are properly generic, can be immutable and covariant, have a proper toString() implementation, have many different implementations with different characteristics, along with many other advantages. So use lists wherever you can.

Performance is a key issue for certain types of applications; e.g., numeric-intensive applications. Here is a table from a paper that I wrote showing the performance differences for Java/double, Java/Double, Kotlin/DoubleArray, and Kotlin/Array<Double>. Note that DoubleArray is more that twice as fast as Array<Double> for this specific benchmark, multiplication of two large matrices.
                          Timed results (in seconds)
    Java            Java             Kotln                     Kotlin
 (double)   (Double)   (DoubleArray)   (Array<Double>)
    7.30            29.83               6.81                      15.82

In deed, since Array will be using the boxed type, it is much slower, but there still is no reason for [Primitive]Array to not be compatible with Array<[Primitive]>, since they are the same type in Kotlin, they can be treated the exact same way.

@JvmInline
value class FloatArrayWrapper(private val array: FloatArray) : ArrayWrapper<Float> {

    override fun get(index: Int): Float {
        return array[index]
    }

    override fun set(index: Int, value: Float) {
        array[index] = value
    }

    override val size: Int
        get() = array.size

    override fun iterator(): Iterator<Float> {
        return array.iterator()
    }
}

fun FloatArray.wrap(): FloatArrayWrapper {
    return FloatArrayWrapper(this)
}

The need for existence of something like this is simply dumb, especially seeing as how Kotlin is made, in part, to avoid the boilerplate nature of Java

1 Like

As mentioned earlier, the reason is compatibility with Java low-level components. I doubt it is technically possible to have a single float array in Kotlin and use it as both float[] and as Float[] and these two types have different characteristics.

You just re-invented List that we mentioned from the beginning :slight_smile: Although, currently stdlib doesn’t provide unboxed primitive list implementations, we need to implement them by ourselves.

In this case primary problem is not the boxing (it is mostly mitigated by GraalVM for example, but memory indirection. But those are technicalities. If you are interested, I discuss it in this talk.

The wrapper class for array partially solves the problem. We use it this way in KMath. The boxing is still there, but indirection is eliminated. My tests with GraalVM show that the losses are mitigated in many cases. But it is hit and miss, of course.

@broot you are not quite correct. ArrayList uses generic array, with all its problems. Specialized wrappers solve some problems. It could be improved further with something like https://youtrack.jetbrains.com/issue/KT-29460/Automatic-specialization-for-final-classes-with-primitive-generic-parameters

Yes, I meant specialized List implementation, similar to above FloatArrayWrapper, but implementing List<Float>. I guess it still requires boxing when reading/writing values from/to, but at least internally it would store data as float[] and not as Object[], it wouldn’t require casting, etc… But I don’t know what would be its performance in relation to FloatArrayWrapper and to ArrayList<Float>.

List is read only, while array isnt. And if you meant a MutableList, then arrays are fixed size. There’s a reason arraylist didn’t fully replace arrays, because there’s a purpose for them.
FloatArrayWrapper stores no additional data, and thus almost 0 overhead, with occasional tiny performance impact with an extra method call, which is, again, minimized because it’s marked a value/inline class, meaning most of the time it can be replaced with just FloatArray, at which point the runtime penalty is 0.
And regarding float vs Float, in kotlin, there is no difference etween float and Float, both being mapped to the Float class, which gets compiled into float or Float depending on the usage, there is 0 reason this can not be done for an array as it has been done for a single variable.

I don’t really understand your point about read-only and auto-growing. These are features of lists, their big advantages over arrays, not their limitations. You can create a mutable list with fixed size if you need, but you can’t create a read-only array, with covariance and auto-growing. I believe the main/only reason why arrays are still there is because of performance benefits over lists and because lists are just types, not the data structures themselves, so it would be impossible to implement lists without arrays.

I think you try to create an utility with two features that are kind of contradicting. If you use FloatArrayWrapper type directly then you get 0 overhead, but then it is no better than using FloatArray directly. Or you can use it as ArrayWrapper<Float>, but then you get something very similar to List<Float> and you get overhead. I believe you can’t have both these features at the same time.

Once again, I believe the reason is technical limitations. When working with single Float value, the compiler can very easily box/unbox it if needed. When working with arrays, there is no simple, fast way to convert between float[] and Float[] - it requires copying the whole array. Could you explain how exactly do you expect it to work internally? Maybe there is a way, but I just don’t see it.

Also, adding to what I said that you can’t have both features of FloatArrayWrapper at the same time. Note that your current implementation of FloatArrayWrapper will always box values when getting/setting them, so it adds similar overhead over FloatArray as specialized List. Why? Because it implements ArrayWrapper. You would need separate functions like primitiveGet to avoid boxing. Which can be done with specialized list as well.

Still, I agree that inline class at least avoids storing additional object in the memory - of course as long as we don’t use it as ArrayWrapper.

  1. no, mutable lists are, by spec, growable, you can “add and remove elements” which is very bad with arrays.
  2. Lists can exist without arrays, that’s exactly what linkedlist is, and in fact, everything that’s not an ArrayList is.
  3. “Using FloatArrayWrapper is no better than using FloatArray directly.” Yes, but I can ask for ArrayWrapper in a receiving method, and pass it the one that uses primitive array, or uses boxed array.
  4. No, there is no boxing involved in FloatArrayWrapper, I would like to see you try and point it out though.

Things like

    public static Float getFloat() {
        return 0.0f;
    }

can exist in java, so boxing on the fly is done by the JVM, meaning having a method which expects Float but returns float has 0 issues whatsoever, meaning your argument about compiler needing to add value boxing in the ending bytecode is incorrect in nature. And there is no case where unboxing has to be done, since anything you can use float for, you can use Float.

Code:

fun test1(arr: FloatArrayWrapper) {
    val z = arr.get(2)
}

Resulting bytecode:

  public static final void test1-wqgjSD8(float[]);
    Code:
       0: aload_0
       1: ldc           #18                 // String arr
       3: invokestatic  #24                 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_0
       7: iconst_2
       8: invokestatic  #28                 // Method FloatArrayWrapper."get-impl":([FI)Ljava/lang/Float;
      11: invokevirtual #34                 // Method java/lang/Float.floatValue:()F
      14: fstore_1
      15: return

As we can see, we received float[] properly, but when accessing an item, it was boxed and immediatelly unboxed. This is because get() overrides a function that is generic. Although, I think the compiler should be able to optimize it better.

1 Like

Alright sure, let’s say the access is boxed, at most it is a tiny overhead, but it doesn’t affect the ram taken up by the array. Which is significantly higher if the array is boxed than unboxed.