Inlinability constraint on traits


#1

Suppose we have a trait:

trait Cast<T> {
    #[trivial]
    fn cast(self) -> T;
}

which intended to be a variant of Into that have extra performance guarantee: the implementation must be trivial and so cast will always be inlined.

Right now the closest is #[inline(always)] but it only works in the impl level and so cannot be used as a constraint.

Do you think it would be a good idea to constraint implementation in this way?


#2

What’s your definition of “trivial”? How would you plan on checking it? Why do you need this guarantee?


#3

I think the easiest thing to do is to either enforce implementations have #[inline(always)] or add them implicitly.

If the compiler then figured out inlining is not possible, errors as usual.

Further restrictions may include a recursive check that a trivial implementation can only call inline functions, directly or indirectly.

There will be some reasons that this will be needed, for example encourage users to use without afraid of performance etc.


#4

Just because something is #[inline(always)], though, doesn’t make it “trivial”. It can still do something arbitrarily expensive.

And if there’s a particularly large implementation and I care about performance, I don’t want it inlined, since that’ll actually reduce performance, typically.

So, again, why is some “constraint” better than a comment “implementations of this are expected to be inexpensive, and thus are called often”?


#5

Even a marker attribute is better than just documentation comments in a way that tools can check it confidently, to enforce any restriction rules it feels should apply. For example, Clippy can warn if it sees potential complicated implementations (loops, for example) in a method marked as trivial.

Ahh. I think making it const fn will do almost all I need? Got to check.


#6

Perhaps right now, but soon it will not:


#7

I think it is not even right now; as one of the RFC reasonal is "we already support recursion in const fn"…


#8

#[trivial] requires presumptive knowledge of all architectures for which code might be generated. Integer multiply is trivial, unless the architecture does not have a multiply instruction (e.g., basic RISC-V), in which case a software loop is likely. Reverse bits is trivial, when the architecture has instructions to support it (e.g., ARM, AArch64).


#9

I’m not really sure that this is a particularly good idea, but for sake of discussion I’ll propose a strawman subset of expressions we can consider “trivial”, which is not closed under composition. These are things I would expect you’d want to inline.

  • as casts
  • Primitive arithmetic, where both operands are identifiers or literals.
  • Single function calls, where all arguments are identifiers or literals.
  • Copying out of a variable or a pointer.
  • Constructing a trivially-destructible type.
  • Branching, maybe?
  • Unsafe intrinsics like ptr::read and mem::transmute (hahaha please no).

I could probably spend the rest of my morning listing these, but I think that’s a pointless exercise. I think what you really want is something like this:

pub unsafe trait Cast<T>: Into<T> {}

Implementing this trait is the assertion that your Into implementation is a cast (or a similar O(1) operation), in a similar way to how implementing Copy asserts that your type’s Clone is just a memcpy. I’m not really sure how this is useful, though…