Guidance on floats – defining types using nested macros

Hi everyone! I’m a Crystal newbie looking for some guidance on how to use Floats (same thing goes for Ints, I guess).

I have seen issues such as:

  • #8111: [RFC] Simplify integers and floats
  • #6626: [RFC] Let the default integer type depend on the platform
  • #8872: [RFC] Disallow integer/float operations between different types

While writing some code I kept wondering:

  • Should I litter my code with “_f32” and “.to_f32” just to save a little bit of memory? (The answer is probably no, as it wouldn’t matter for what I’m doing right now.)
  • Does that make my code a couple of nanoseconds faster?
  • Can I be sure that Float32 is always faster than Float64 on all architectures?
  • Do Crystal’s Float64 work on 32-bit architectures?
  • Would I confuse users of a library by requiring Float32 in the public interface, as Float64 is the default?
  • Would any potential performance gains be far outweighted by conversions between Float32 and Float64?

Sure, I could run some benchmarks, but I’m more asking about the general direction, as the API docs for Float don’t tell me anything such as “In most cases, just use the default and call it a day.” or “You should always use Float32 unless you specifically need Float64.”.

What I’m currently doing is this:
In each source file I’m requiring a “type_macros.cr” file containing a macro that in turn defines macros:

macro mylibrary_type_macros()
  
  # `f(1.0)` => `1.0_f32`.
  #
  private macro f(val)
    \{{val}}_f32
  end
  
  # `to_f(1.0 + 1.0)` => `(1.0 + 1.0).to_f32`.
  #
  private macro to_f(val)
    ( \{{val}} ).to_f32
  end
end

So when you call mylibrary_type_macros() in the top-level scope of
each source file, you end up with the private macros, so their scope is limited to one source file.

An then I have a

macro mylibrary_float_type_alias()
  alias F = Float32
end

in the “type_macros.cr” file that I can call in the main module of my library.

Does what I need, and if I ever wanted to switch to Float64 (or just Float, or maybe PlatformspecificFloat, or maybe FastFloat, or whatever the future may bring) I could do that in just one file.

But should I really have to use nested macros just to get the types that I want?
Is that a common pattern?

IMO I think you’re overthinking this and it feels like such a micro optimization. Just use the default float type and call it a day unless you really have a reason. Introducing custom macros and stuff just to convert one type to another just introduces another level of abstraction that the reader has to figure out what it means, when at the end of the day I can’t imagine it making that big of a difference. Esp when doing math with literal values given LLVM probably just does the math at compile time and makes it all moot anyway.

I also don’t even think 32bit is really an officially support platform anymore for Crystal either? At least according to Platform Support - Crystal.

2 Likes

It would be a helpful if you could tell a bit about your use case. The length that you go for floating point typing might hint at some special case scenario, but the questions do not.

General advice:
Use Float32 when you need 32-bit semantics and Float64 when you need 64-bit semantics.
If it doesn’t matter, it’s best to follow the default, Float64.

There maybe some very specific use cases where you might need to take other properties into account, but if that’s the case, you would know about it.

The bit size of Float32 is smaller, but that doesn’t necessarily use less memory. Depends on packing and alignment. Don’t worry about this unless compact memory representation is an explicit requirement.

Floating point arithmetics are highly optimized in modern processors and wether it’s operating with 32-bit and 64-bit width should usually take the same amount of cycles.

Yes.

If there’s no specific reason in the domain model to use Float32, yes it would be confusing.

1 Like

Alright. Thank you guys!
At least I learned some stuff about macros. :-)

I think the takeaway is:

  • For CPU or memory intensive stuff where you use tons of objects or other specific use cases: Put some thought into it, benchmark, measure memory usage …
  • In other cases: Just use the default.
1 Like