Custom ranged Int

Is it possible to create a custom ranged Int with a near-native performance? Currently I thought to use such structure, but I concern about its resource consumption even if make min, max static:

class IntRanged(value: Int, val min: Int, val max: Int) {
    var value: Int = value
        set(value) {
            field = inRange(value, min, max)
        }
}

fun inRange(value: Int, min: Int, max: Int) = max(min(max, value), min)

What’s your concern about the performance? max() and min() are very simple and fast functions. Anyway, I think simple if-elses would be both easier to read and potentially a little more performant than that.

Is even more readable

2 Likes

Here’s a DSL-y answer:

@JvmInline value class RangedInt<T: Range<T>> internal constructor(val value: Int)

interface Range<Self: Range<Self>> {
	val min: Int
    val max: Int
    fun rangedInt(value: Int): RangedInt<Self> = RangedInt(value.coerceIn(min, max))
    operator fun RangedInt<Self>.plus(other: RangedInt<*>) = rangedInt(value + other.value)
    operator fun RangedInt<Self>.plus(other: Int) = rangedInt(value + other)
    operator fun RangedInt<Self>.minus(other: RangedInt<*>) = rangedInt(value - other.value)
    operator fun RangedInt<Self>.minus(other: Int) = rangedInt(value - other)
    //TODO: implement other operators :)
}

object Positive: Range<Positive> {
	override val min = 1
    override val max = Int.MAX_VALUE
}

object ZeroToFive: Range<ZeroToFive> {
	override val min = 0
    override val max = 5
}

data class MyClass(var myPositiveInt: RangedInt<Positive>, val myOffset: RangedInt<ZeroToFive>)

fun main() = with(Positive) {
    val myClass = MyClass(rangedInt(3), ZeroToFive.rangedInt(7))
    println(myClass)
    myClass.myPositiveInt += myClass.myOffset
    println(myClass)
    myClass.myPositiveInt -= myClass.myOffset
    myClass.myPositiveInt -= 10
    println(myClass)
    myClass.myPositiveInt = rangedInt(-42)
    println(myClass)
}

Basically, every RangedInt knows what its type of range is so that when you create it, it can be sure that you didn’t go beyond its range. With some reflection magic we could also remove the need of having the range as a receiver to rangedInt and the operators on it, but if performance is key then having the range as a receiver is a small annoyance.
This should have near-native performance because of value classes. The only (very very minor) overhead is that creating a RangedInt and doing any operation on it will call the apt method on the range, which is one extra function call. That should be marginal in the grand scheme of things.

1 Like

My concern is the custom IntRanged class consumes more memory or CPU resources than a primitive and it can hit performance even with 1 mln usages. Is it not true?

Override basic Int or Short bounds looks like a default way, but I didn’t achieved that solution. Also I didn’t check if the number passed the bound, should it jumps to other one (like in C?) e.g. 127 + 1 = -128

Hmm, but how do you want to store 3 ints and still use the same amount of memory as a single int?

public fun Int.coerceIn(minimumValue: Int, maximumValue: Int): Int {
    if (minimumValue > maximumValue) throw IllegalArgumentException("Cannot coerce value to an empty range: maximum $maximumValue is less than minimum $minimumValue.")
    if (this < minimumValue) return minimumValue
    if (this > maximumValue) return maximumValue
    return this
}

It uses comparisons while min/max use nativeMath. Doesn’t it hit performance?

That’s why I didn’t use a custom class yet. And Ints count can be dropped to 1 with statics or predefined shells like @kyay10 wrote.

You have an weird angle on performance.

The answer, as always, is: “profile in your case and find out”.

I find it hard to believe that a bottleneck for some application is computing maxima and minima between numbers, but if just do that, maybe.

1 Like

Why use already underperforming method if it’s possible to use well-optimized one same way?
But I believe, Kotlin devs already used optimal methods and coerceIn could have same performance.

That’s practically impossible on the JVM.

That’s up to you, you’d then basically be using modular arithmetic to achieve that. My DSL solution should be easily modifiable to fit that :slight_smile:. That’s probably your best option if performance is a top priority, but again, as others have mentioned, profile your case first and see if it’s that crucial.

What about the performance? Could you vaguely tell how much it can drop comparing with just using Int? E.g. MutableMap<String, Int> vs Map<String, RangedInt<ZeroToFive>>.

The range itself isn’t what you store, it’s the RangedInt. The issue is, with value classes, they get boxed when placed inside a Map, so in fact there would be some performance drop here. Same as if you stored Int inside a Map though, so there’s no performance difference compared to a Map<String, Int>

Yeah, made a correction. So, the performance drop is the reason I think to make a class with plain MutableMap<String, Int> and all setters with bound calculations inside it.

Again, MutableMap<String, RangedInt<SomeRange>> would have the same performance as MutableMap<String, Int> because both cases would box, but the benefit of RangedInt is the safety that comes along with it which ensures that every instance will have an int value that’s within SomeRange. The only real downside of RangedInt is that you’ll need to surround your code with with(SomeRange) { ... } so that you have access to the operators and to the rangedInt function

Seems, I misunderstood you first time. Ok, if performance is same, I’ll check how convenient is to use such structures.

1 Like

Did you measure any of this?

Again, performance of 99.99% of the code is not much important. The only thing that matters is how optimized are the things that really take time.

1 Like

No, I didn’t. Otherwise I didn’t ask much about it :).

But no one else can.

As I mentioned a few times, this depends on your use case, and what else happens in your code - which we don’t know.

Are you writing a program which just takes the min and max of integers in a loop? Well then someone else can probably tell, but that is unlikely to be your use-case.

1 Like