Inferring MethodInstance from code_llvm

I am trying to track down some performance issues by tracing through the output of code_llvm. Whenever the LLVM code contains a call to a Julia function, I would like to figure out which MethodInstance of that function is getting called, and then recursively analyze the output of code_llvm for that MethodInstance. However, I am unable to determine the MethodInstance in cases where different modules have functions with the same name, and my current strategy is unreliable for functions with multiple methods.

Here are some examples of relevant LLVM code:

call double @"j_^_18287"(double %0, i64 signext %10)
call i64 @j_mapreduce_impl_18993({}* nonnull %0, i64 signext 1, i64 signext %arraylen, i64 signext 1024)
call i64 @julia_mapreduce_impl_19022({}* %0, i64 signext %1, i64 signext %34, i64 signext %3)
call nonnull {}* @j1_print_to_string_14966({}* inttoptr (i64 5467587216 to {}*), {}** nonnull %.sub, i32 4)

Through some exploration, I’ve found that the LLVM name of a Julia function starts with @julia_ when the function is called recursively, @"j_ when the function name has non-standard characters, and @j_ or @j1_ otherwise. I have not been able to figure out what the string of numbers at the end of each function name corresponds to, though.

Does anyone know what this string of numbers is? Is it a unique identifier for each compiled MethodInstance, and can I obtain this identifier without delving into the underlying C++ code? If not, is there some other information in the code_llvm output that I could use to infer which MethodInstance of a function is getting called?

Is there a particular reason why @code_warntype or other variants are unsuitable for the analysis?

There are also tools like @which to find the concrete method used and then Cthulhu.jl or JET.jl to get deeper insights.

1 Like

We’ve already run our code through JET, Aqua, Profile, and SnoopCompile. We’ve used Cthulhu to remove most type instabilities, and type inference is no longer a significant performance issue for us. I’m looking at @code_llvm, rather than @code_warntype, because I am trying to solve problems that depend on the rest of the compilation process:

  • Some of our operations have very long code generation times (which show up as empty gaps in SnoopCompile flame graphs), and a few edge cases can cause out-of-memory issues during compilation on GPUs. I would like to understand what is going on at the level of LLVM code generation, if this is possible.
  • We are interested in improving performance on GPUs by introducing loop fusion and shared memory caching to small groups of operations. I would like to automate the process of deciding which operations to fuse and where to insert cache synchronization pauses. If possible, I want to compute rough estimates of quantities like the register pressure and code generation time for a fused kernel by analyzing the LLVM code for its individual sub-kernels.

On my phone so unable to give a full explanation, but the prefix of the function is related to it’s calling convention (e.g. are values passed boxed or unboxed, modulo some placeholder names for codegen).

The suffix is a global counter to disambiguate each llvm function. (Each function has many methods, each methods can have many instantiation s, each instantiation can be compiled multiple times).

If you see an raw integer embedded in the IR that is converted with inttoptr it more often than not is a global Julia object that is valid during the current session (so you can do unsafe_object_from_ptr) on it.

1 Like

Looks like I can use pointers in the output of @code_llvm to get either the MethodInstance or the Function being called in certain cases. For example, I can use unsafe_pointer_to_objref(Ptr{Nothing}(<integer>)) on the integers embedded in the following lines:

call nonnull {}* @j1_print_to_string_14966({}* inttoptr (i64 5467587216 to {}*), {}** nonnull %.sub, i32 4)
call nonnull {}* @ijl_apply_generic({}* inttoptr (i64 4761373168 to {}*), {}** nonnull %.sub, i32 2)
call nonnull {}* @ijl_invoke({}* inttoptr (i64 4751558928 to {}*), {}** nonnull %3, i32 1, {}* inttoptr (i64 4464873632 to {}*))

Dereferencing the first argument in each of these cases gives the Function being called/applied/invoked:

print_to_string (generic function with 1 method)
println (generic function with 3 methods)
__throw_rational_argerror_zero (generic function with 1 method)

In the last case, I can also dereference the final argument to get a specific MethodInstance:

MethodInstance for Base.__throw_rational_argerror_zero(::Type)

Although getting the MethodInstance for every call would make things a lot easier, getting the Function on its own should be enough for what I’m trying to do. This is because the output of @code_llvm has debug information that I can use to reconstruct the stacktrace of any function call. During the analysis of my program (which I’m doing with Cassette.jl), I can check the stacktrace of each call to determine whether I need to step into it or whether it was inlined into previously analyzed LLVM code. There are probably some edge cases where this won’t work (like if a function calls two different methods of another function, one of which gets inlined while the other doesn’t), but it should be enough to get started.

Is there any chance I can get pointers to all the other Julia functions being called from the LLVM code? Specifically, is it possible to get more information about calls that look like this:

call double @"j_^_18287"(double %0, i64 signext %10)
call void @"j_#84_13401"([4 x double]* noalias nocapture noundef nonnull sret([4 x double]) %1)
call i64 @j_mapreduce_impl_18993({}* nonnull %0, i64 signext 1, i64 signext %arraylen, i64 signext 1024)
call i64 @julia_mapreduce_impl_19022({}* %0, i64 signext %1, i64 signext %34, i64 signext %3)

If there aren’t raw pointers to the relevant functions floating around in the output of @code_llvm, I could also look at @code_native or somewhere else.

Those are fully devirtualized native calls. You need to look at code_lowered for those (like Cthulhu does)

Inlining only happens on the Julia IR level so pre-codegen is probably the time you want to inspect the IR