Crystal: Bool#to_unsafe can be improved

Created on 3 Mar 2019  路  7Comments  路  Source: crystal-lang/crystal

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.

invalid

Most helpful comment

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
}
...

All 7 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

will picture will  路  3Comments

oprypin picture oprypin  路  3Comments

ArthurZ picture ArthurZ  路  3Comments

Papierkorb picture Papierkorb  路  3Comments

TechMagister picture TechMagister  路  3Comments