In fact objc_msgLookup actually ends up being faster in a some micro benches since it plays a lot better with modern CPU branch predictors: objc_msgSend defeats them by making every call site jump to the same dispatch function, which then makes a completely unpredictable jump to the imp. By using msgLookup you essentially decouple the branch source from the lookup which greatly improves predictably. Also, with a “sufficiently smart” compiler it can be win because it allows you to do things like hoist the lookup out of loops, etc (essentially really clever automated IMP caching tricks).
There are also a number of minor regressions, like now you are doing some of the work on a stack frame (which might require spilling if you need a register, vs avoiding spills by using exclusively non-preserved registers in an assembly function that tail calls). In the end what kills it is that the profiles of most objC is large flat sections that do not really benefit from the compiler tricks or the improved prediction, and the added call site instructions end up in increased binary sizes and negative CPU i-cache impacts.
However, I'll happily accept that these are probably pretty small costs, especially since so much of it is just register gains which probably result in cost-free renamings in the hardware. It makes sense that the i-cache impact is more important.
({
static SEL last_isa = NULL;
static IMP last_imp = NULL;
if (object->isa != isa){
last_isa = object->isa;
last_imp = lookup(object->isa, sel);
}
last_isa(object, selector, arguments...);
})
(modulo the fact that you cannot generate this by dumb string substitution without compiler extension like gcc's ({...}))Smalltalk/X takes this to the extreme by compiling all sends into code like:
{
static struct cache = {.imp = &magic_global_method, .class=NULL}
cache->imp(&cache, object, selector, arguments...);
}
And generates something like this into every method prologue: if (cache && self->isa != cache->class){
cache->class = self->isa;
cache->imp = lookup(object, selector);
return cache->imp(NULL, object, selector, arguments...)
}
It looks convoluted and uses one additional word of stack space per call, but does not contain any unpredictable indirect branches in the fast path (and in fact reduces overall code size as it can be expected that there are many more sends than methods).Yes there is: va_list.
> no way to express forwarding an arbitrary return value
Of course there is, and lots and lots of language runtimes implemented in C use those ways. Usually it boils down to having a base type called Object or Value and passing around pointers to that. In fact, from your example it looks like the "id" type is meant to play this role.
This is not syntax checked, but the code above would be something like:
Object *objc_msgSend(id self, SEL cmd, ...) {
fptr = ...lookup code...
va_list args;
va_start(cmd, args);
Object *result = fptr(self, cmd, args);
va_end(args);
return result;
}
Yes, this can be faster in assembly, but it's not true that there is no way to express this. (Unless I'm misunderstanding something.)Using varargs and passing va_list into the method would mean that your method is no longer a plain C function with the declared parameters plus two hidden parameters. It's now a different sort of beast, and has to use va_ calls to extract the values. This would require a lot more work in the method, and hurt performance.
Returning everything as an object would mean boxing and unboxing primitive values at every call, which would be horrendously inefficient.
And if you don't care about extracting every last bit of performance, it's much easier to do the lookup approach I discussed than it is to faff around with varargs and wrapping return values.
This is plain false.
This actually manifests in errors if you directly call objc_msgSend, which is why in order to guarantee direct codeine you need to cast objc_msgSend to the actual prototype you want[2]:
"An exception to the casting rule described above is when you are calling the objc_msgSend function or any other similar functions in the Objective-C runtime that send messages. Although the prototype for the message functions has a variadic form, the method function that is called by the Objective-C runtime does not share the same prototype. The Objective-C runtime directly dispatches to the function that implements the method, so the calling conventions are mismatched, as described previously. Therefore you must cast the objc_msgSend function to a prototype that matches the method function being called."
1: https://developer.apple.com/library/content/documentation/Xc... 2: https://developer.apple.com/library/content/documentation/Xc...
Citing the bastard architecture of iOS isn't really making the case for "usually".
On x86-64, the caller also has to set %al to the number of vector registers used for the call, and the compilers I've seen always check %al and conditionally save those registers as part of the function prologue. Cheap, but not "zero cost."
We could probably argue this some more but I suggest you simply try it with a compiler..
https://gist.github.com/mikeash/ce38d3a77b88734a9e0e9dc3f352...
You'll notice how `normal` takes all of its arguments out of registers `x0` through `x7` and places them on the stack for the call to `printf`. And you'll notice how `vararg` plays a bunch of games with the stack and never touches registers `x1` through `x7`. (It still uses `x0` because the first argument is not variadic.)
On the caller side, observe how `call_normal` places its values into `x0` through `x7` sequentially and then invokes the target function, while `call_vararg` places one value into `x0` and places everything else on the stack.
So, no, it looks to me like varargs very much change the calling convention.
Please don't say things like "This is plain false" when you say things like this which are, well, just plain false.
Come on. Taking an argument list in an argument list object and passing that argument list as an argument list to a function is exactly what this was about. It's not a "simulation". It's a C feature for capturing and passing around argument lists. It actually does it.
> has to use va_ calls to extract the values. This would require a lot more work in the method
Loads from the stack at fixed offsets for every argument instead of having some of the arguments in registers and loading others from the stack at fixed offsets. Yes, that is more work.
> Returning everything as an object would mean boxing and unboxing primitive values at every call, which would be horrendously inefficient.
True, the runtimes I was thinking of box many things. But you can type-pun pointers to other things, so you don't necessarily have to box everything. I don't know enough about Objective-C's constraints, but I do note that the linked article did talk about using tagged pointers already.
You cannot implement this with plain C. That's a simple fact. If your idea doesn't work for a method that, say, takes a double and returns an fd_set as raw C types then your idea doesn't do what objc_msgSend does.
Yes, you can shift the problem around and come up with a system that you can implement in plain C. I outlined one approach for that, and you've outlined another. Nothing wrong with that, but it's not solving the same problem. So feel free to elaborate on other ways that it could be done, but don't tell me I'm wrong because you've come up with a way to solve a similar but different problem.
And yes, it is significantly faster. Avoiding writing assembly seems like an awfully odd goal to have for a language runtime.
But fptr doesn't take a va_list - it takes the actual arguments.
Obviously you could come up with other ways of passing arguments to obj-c methods which would make it possible to implement your message send function in pure C, but a message send function which passes the arguments as a va_list is not objc_msgSend(), and that says nothing about whether or not objc_msgSend() could be implemented for the design they did go with in pure C.
Anyway, I think I've said all I'm going to say here.
However, Objective-C does not require objc_msgSend. With objc_msgSend, a method call site generates code that's essentially equivalent to (for a method that takes one object parameter and returns void):
In other words, take objc_msgSend, cast it to a function pointer of the correct type, and call it.Instead of objc_msgSend, the runtime can provide a function which looks up the method implementation and returns it to the caller. The caller can then invoke that implementation itself. This is how the GNU runtime does it, since it needs to be more portable. Their lookup function is called objc_msg_lookup. The generated code would look like this:
However, each call now suffers the overhead of two function calls, so it's a bit slower. Apple prefers to put in the extra effort of writing assembly code to avoid this, since it's so critical to their platform.