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.
@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.
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
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?
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 up to you, you’d then basically be using modular arithmetic to achieve that. My DSL solution should be easily modifiable to fit that . 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
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.