Saline: Saturating Arithmetic in Crystal

Hey, all! I’ve just released saline, a shard for using saturating arithmetic in Crystal. This is something I’ve been meaning to do, but some recent discussion motivated me to make a shard available.

I’d love to get some feedback if anyone has any ideas (especially if you’re actually going to be using the shard).

5 Likes

Hahaha awesome work man

1 Like

Cool :slight_smile:

For the readme example, how about something like

n = Saturating(Int32).new(Int32::MAX - 2)
n += 20 # => 2147483647 (Int32::MAX)

m = Saturating(Int32).new(Int32::MIN + 3)
m -= 30 # => -2147483648 (Int32::MIN)

At first I was confused that you’d pass the (arbitrary) maximum value you want to allow to .new :D

API wise for a shard like this I wouldn’t mind adding itself to Int, so providing something like 0.to_si32 etc.

I’m not sure why you’d want to keep multiple implementation around, the intrinsics version should beat them all :) Meanwhile an implementation using wrapping operators (&+ etc) and checking whether the value wrapped around (by keeping value from before the operation around and comparing) should be slightly more performant as it avoids the stack unwinding :)

1 Like

@RespiteSage using begin/rescue to recover from exception will probably be slow. In Arithmethic overflow should not raise an exception I shared the following snippet that uses llvm intrinsics for the task

lib LibIntrinsics
  fun sat_uadd8 = "llvm.uadd.sat.i8"(a : UInt8, b : UInt8) : UInt8
end

module Intrinsics
  def self.sadd(a : UInt8, b : UInt8) : UInt8
    LibIntrinsics.sat_uadd8(a, b)
  end
end

pp! a = 100_u8                     # => 100
pp! a = Intrinsics.sadd(a, 100_u8) # => 200
pp! a = Intrinsics.sadd(a, 100_u8) # => 255

Another idea that I would like to throw is that I think it should be possible to use macros to rewrite calls to + to some custom function like Intrinsics.sadd.

So Saline.eval(1 + 2 + 3) would compile to Intrinsics.sadd(Intrinsics.sadd(1, 2), 3) or something similar.

1 Like

8 posts were merged into an existing topic: Arithmethic overflow should not raise an exception

This is a good point. I’ll update the README. In particular, my number choice seems to have caused confusion already…

I don’t like throwing stuff away… However, if I’m able to keep an API that’s easy to use (like, I’d argue, the current one), I’m fine only using the LLVM-based implementation.

Yeah, that’s on my to-do list. :slight_smile: I saw your code snippet, and I was planning on using something like that. However, I think wrapping it in a type will probably still be a friendlier API.

OT

I had worked on saturating arithmetic a little before, but a large part of the reason I made this shard was so that you could use it. I’m not asking for overflowing gratitude, but it would be nice if you weren’t rude about this thing that was made for you.

This is a product of my bad example in the README (see @jhass’s post above). In a call Saturating(T).new(x), the T is obviously the type, and the x is the value you want the variable to have (not the max). I’ll be changing the README to make that more clear.

I don’t think this is possible, but I’ll look into it.

People have discussed this with you, and I’m not going to rehash this argument. This thread is not a place to argue that this shard shouldn’t exist because you think the language should already work like this.

It should be possible, just don’t supply a generic arg and T should get inferred from the argument.

EDIT: See Generics - Crystal.

Before introducing the &+ and others operators for wrapping operations there were some proposal and discussion about how wrapping types can be used. If interested, check https://github.com/crystal-lang/crystal/pull/6223 .

1 Like

@RespiteSage That was so nice of you to help out :heart: You did nothing wrong and it is one person acting in a hostile manner. Your contribution to the conversation by creating a solution is a great idea!

2 Likes

Saline v0.2.0 has been released!

The documentation is still pretty lacking, but the big thing is that Saturating is much faster in almost every case due to a new LLVM-backed implementation. It’s pretty complicated under the hood, and it’s currently only properly implemented for Int* and UInt* types, but my benchmarks show most operations taking less than 1.2x as long as those operations normally take for primitives. The worst slowdowns, for multiplication (there’s no LLVM backing for saturating multiplication), still take less than 10x as long as the non-saturating operations on primitives.

If anyone is using Saline or uses it in the future, please let me know. I’m probably not going to put much more work into it unless/until I know that people need feature or documentation improvements for it.

2 Likes