I'm very sure Bool's to_unsafe can be optimized/improved. Every boolean in Crystal is represented as a one bit integer in LLVM (i1) and that can only be 1 (true) or 0 (false). So to_unsafe should become a special instance variable or method which has access to this internal LLVM integer. Currently to_unsafe is implemented using branching (if true, return 1, if false, return 0) but that's very inefficent in contrast to that the result of to_unsafe is already present in LLVM and doesn't even have to be computed.
And that would also make it possible to optimize other methods like Number#sign. Currently sign also uses branching but that can be avoided with for example (x > 0).to_unsafe - (x < 0).to_unsafe which would be faster than the current sign implementation when to_unsafe is optmized, I think.
How does it actually end up in the generated code? LLVM is pretty smart, and at least in C++, a similar bool conditional is converted to a mov, and the sign example is converted to around 6 branchless assembler instructions.
On the other hand, a version like what you propose generates one fewer instruction, but it also involves subtractions which take up more cycles.
TL;DR: Don't try to outsmart the optimizer.
Don't worry, LLVM is smart.
lib LibC
fun exit(code : Int32)
end
struct Int32
def self.new(value)
value.to_i32
end
end
struct Bool
def to_unsafe : Int32
Int32.new(self ? 1 : 0)
end
end
a = true.to_unsafe
LibC.exit a
crystal build tmp.cr --prelude=empty --release --emit=llvm-ir
md5-c90d8527e5e4f377bfcd083d57193f68
...
define void @__crystal_main(i32 %argc, i8** %argv) local_unnamed_addr !dbg !4 {
alloca:
store i8** %argv, i8*** @ARGV_UNSAFE, align 8
tail call void @exit(i32 1), !dbg !9
ret void, !dbg !9
}
...
lib LibC
fun exit(code : Int32)
end
struct Number
def sign
self < 0 ? -1 : (self == 0 ? 0 : 1)
end
end
LibC.exit -100.sign
define void @__crystal_main(i32 %argc, i8** %argv) local_unnamed_addr !dbg !4 {
entry:
store i8** %argv, i8*** @ARGV_UNSAFE, align 8
tail call void @exit(i32 -1), !dbg !9
ret void, !dbg !9
}
this -100 is hardcoded
for value
LibC.exit ARGV_UNSAFE.value.value.sign
define void @__crystal_main(i32 %argc, i8** %argv) local_unnamed_addr !dbg !4 {
entry:
store i8** %argv, i8*** @ARGV_UNSAFE, align 8
%0 = load i8*, i8** %argv, align 8, !dbg !9
%1 = load i8, i8* %0, align 1, !dbg !9
%2 = icmp ne i8 %1, 0, !dbg !12
%3 = zext i1 %2 to i32, !dbg !12
tail call void @exit(i32 %3), !dbg !17
ret void, !dbg !17
}
Before making such claims, please verify the generated assembly (not just LLVM IR code) for code compiled with --release which turns on aggressive LLVM optimizations, that does a very good job in such situations.
@r00ster91 do you agree we can close this?
Yes, thanks for the explanation.
Most helpful comment
Don't worry, LLVM is smart.