improving constant/type uniquing

A few additional thoughts on PR1210:

I can think of three approaches to addressing this issue. The first approach is the one outlined in PR1210, which is to have the key have enough knowledge of the internals of the Type to be able to use the internal type array as the data for the key.

The second approach is similar, which is to factor out the type array as a separate, immutable object which is shared by both the key and the type object. That is, you first create the type array, use that to look up the type, and if it doesn’t find it, clone the type array and use it to construct a new type object, then use the same type array as the key.

This is effectively what FoldingSet does. The actual bits are stored in the type if it exists. If it doesn’t exist, the input vector of element types is hashed by the profiling function, and then the hash value is looked up. If there is no match, a type is created. The profile is designed to typically live on the stack to avoid a heap allocation.

Note that you can’t use the same type array that you used for the lookup, because otherwise the get-or-insert function would sometimes have to take ownership of the type array depending on whether the type existed or not, which would be confusing.

Right: the idea is that a lookup (which hits an already created value) should not do a heap allocation.

Another way around the ownership problem is to allocate all type arrays as part of the memory pool associated with the LLVMContext, and then ‘intern’ the type arrays so that they can be compared via pointer rather than by value. The type arrays go away when the context does, so you don’t have to worry about who is responsible for freeing it. The downside is that you now have to do two hash lookups for the type object’s get-or-create method.

I don’t really understand what you mean by this. You still need to hash on the identity of the type and the subclass of type still needs to have accessors for its elements.

-Chris

A few additional thoughts on PR1210:

I can think of three approaches to addressing this issue. The first approach is the one outlined in PR1210, which is to have the key have enough knowledge of the internals of the Type to be able to use the internal type array as the data for the key.

The second approach is similar, which is to factor out the type array as a separate, immutable object which is shared by both the key and the type object. That is, you first create the type array, use that to look up the type, and if it doesn’t find it, clone the type array and use it to construct a new type object, then use the same type array as the key.

This is effectively what FoldingSet does. The actual bits are stored in the type if it exists. If it doesn’t exist, the input vector of element types is hashed by the profiling function, and then the hash value is looked up. If there is no match, a type is created. The profile is designed to typically live on the stack to avoid a heap allocation.

Note that you can’t use the same type array that you used for the lookup, because otherwise the get-or-insert function would sometimes have to take ownership of the type array depending on whether the type existed or not, which would be confusing.

Right: the idea is that a lookup (which hits an already created value) should not do a heap allocation.

Another way around the ownership problem is to allocate all type arrays as part of the memory pool associated with the LLVMContext, and then ‘intern’ the type arrays so that they can be compared via pointer rather than by value. The type arrays go away when the context does, so you don’t have to worry about who is responsible for freeing it. The downside is that you now have to do two hash lookups for the type object’s get-or-create method.

I don’t really understand what you mean by this. You still need to hash on the identity of the type and the subclass of type still needs to have accessors for its elements.

I’ll try to explain it a different way. Imagine you have a single folding set, independent of any type subclass, which takes as input an array of types (the key), and returns a pointer to an immutable vector or tuple of those types - much like an interned string. That pointer can then be used as a key to get a StructType or a FunctionType or any other type which has more than one type parameter, and all derived types now represent their type parameters as a single pointer, either to a type or to a tuple. It also means that type “{ int; int; int; }” shares the same type parameter array as “int (int, int)”, both pointing to a tuple of 3 ints. Each subtype still has its own hash map that enables lookup of a subtype via it’s type parameters, however there’s no need to dereference the tuple pointer during this second lookup, you only need to compare the two pointers, not what they point to.

I’m not certain that this approach has any real benefit, however, given the normal usage patterns of looking up types in LLVM. I was merely including it for the sake of completeness… since normally subtypes are interned anyway, and since most subtypes take only a single type parameter, the benefits of separately interning the type parameter array probably aren’t worth the additional complexity.