Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HPy_CallTupleDict, HPyCallable_Check and HPyTuple_Check. #147

Merged
merged 16 commits into from
Jan 11, 2021

Conversation

hodgestar
Copy link
Contributor

Implements the simpler pythonic calling API for #122.

@hodgestar hodgestar changed the title WIP: Add HPy_Call, HPy_CallObject, and HPyCallable_Check. Add HPy_Call, HPy_CallObject, and HPyCallable_Check. Dec 21, 2020
@hodgestar
Copy link
Contributor Author

Ready for review.

Copy link
Collaborator

@antocuni antocuni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the PR looks good.
I'm not 100% sure that we can to call it HPy_Call, though. On CPython it is called that way because it matches the signature of tp_call, but in HPy we will probably end up with a different signature, so it will be a bit weird.

One option could be to call it HPy_LegacyCall maybe? This would leave HPy_Call free for whatever signature we think it's more appropriate later.

def f(a, b):
return a + b

assert mod.call(f, (1, 2)) == 3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should also test what happens if you pass something which is not a tuple and/or a dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I'll add some.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The answer is that C Python segfaults in some cases and works fine in others (e.g. a list of args). I've added tests and changed the HPy implementation to check the types of handles rather than segfault.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if doing these extra check costs any performance. Do you feel like adding microbenchmarks?
If they are costly, we could do the check only in debug mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing not a whole lot, but I'll add the microbenchmarks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Microbenchmarks added.

@hodgestar
Copy link
Contributor Author

hodgestar commented Dec 29, 2020

@antocuni I don't mind a different name, but:

  • I'm against "Legacy" because I don't know if it really is legacy or not. My guess is that it will continue to be fine to use it for a long time.
  • I like "HPy_Call" because it maps obviously to "PyObject_Call" which it replaces, so it's for people porting code to know what to use and how it works. A later "HPy_Call" that does things differently to "PyObject_Call" would add to the confusion.

I don't have another good name to suggest really, so I'm a bit stuck on this point. Any other suggestions? If it's not going to be the same name as "_Call", I think the name should try describe the method.

@antocuni
Copy link
Collaborator

I agree that no solutin is perfect, it's the usual tradeoff between keeping compatibility with the old API and designing a better one.
Let me try to explain why I think this case is "more special" than others; we have a general guideline than HPy_Foo should map precisely to Py_Foo so I agree we should do the same for HPy_Call. However, there are at least two use cases which comes to my mind:

  1. call something with the same arguments that we just received from the caller
  2. call something with a tuple which we just created

Your version of HPy_Call addresses point 2, but doesn't help with point 1. So people who are porting code doing 1 will be annoyed/confused anyway because simply adding the H in front of the call doesn't work. OTOH, if we make HPy_Call the equivalent of PyObject_VectorCall, point 1 will work out of the box, because this is what they receive from the caller.

As for point 2, I claim we should discourage it :). In most cases creating a tuple just to call a function is inefficient and unnecessary: on PyPy, we just don't need the tuple at all and CPython is migrating toward vector call and thus more native functions support the faster calling convention. I don't know about GraalPython, maybe @fangerer or @timfel have opinions?
Moreover, creating the tuple itself is different in HPy since we have TupleBuilder now: so people might end up doing more work to use "your version" of HPy_Call, just because it was named so conveniently :)

Of course we should support a way to call things "the old way", that's why I proposed HPy_LegacyCall. But it should be a conscious decision, not something which is done "by chance" only because it was very convenient to add the H in front of it.

I tried to do a quick grep on the numpy source code to see how it is used in the wild. Excluding the cython-generated sources, there are 13 usages of PyObject_Call: 7 of them are doing what I described in point 1, and 6 are creating new tuples. So, whatever decision we take we are going to break ~50% of the usages anyway.

@hodgestar
Copy link
Contributor Author

For clarity, are you proposing to have the names be HPy_CallLegacy and HPy_CallObjectLegacy? Or something else?

@TeamSpen210
Copy link

What about HPy_CallTuple() and HPy_CallTupleDict()? That makes the requirement for a tuple/dict explicit, and leaves HPy_Call() open for an optimised/preferred calling convention later.

@antocuni
Copy link
Collaborator

For clarity, are you proposing to have the names be HPy_CallLegacy and HPy_CallObjectLegacy? Or something else?

CPython implements PyObject_CallObject as PyObject_Call(..., NULL), so it might be fine to only add the second as a "native" HPy API function. If we really want, we can provide a small helper to implement the equivalent of PyObject_CallObject which lives just in a .h as a static inline function (IIRC we have already discussed the idea of having a header which contains implementation of various functions which are not part of the official API but that we want to provide to make porting easier).

So, let's implement only the equivalent of PyObject_Call. Possible names:

  1. HPy_LegacyCall
  2. HPy_CallLegacy
  3. HPy_CallTupleDict

I don't have any strong preference. HPy_CallTupleDict is kind of nice because it's very explicit.

@hodgestar
Copy link
Contributor Author

hodgestar commented Jan 4, 2021

So, let's implement only the equivalent of PyObject_Call.

Note that in the old C API, PyObject_CallObject allows args to be NULL, but PyObject_Call does not. I could work around that in HPy_CallTupleDict by creating an empty tuple if needed?

@timfel
Copy link
Contributor

timfel commented Jan 5, 2021

HPy_CallTupleDict is kind of nice because it's very explicit.

I like explicit, too. In general we would of course like to avoid creating the tuple for calls if we can. When not running in ABI mode (i.e., everything is compiled and run as bitcode and jitted on Graal) we can sometimes remove the tuple allocation if we're lucky, but a) that's more work for the optimizer and b) it's better if ABI mode doesn't incur a performance hit on GraalPython that would push users towards compiling per VM yet again.

@hodgestar
Copy link
Contributor Author

I've unified things into a single HPy_CallTupleDict. The API seems nice now, but I'm not sure I'm wild about the amount of custom C code in HPy_CallTupleDict or that it calls back to multiple different C Python API methods.

Thoughts?

@antocuni
Copy link
Collaborator

antocuni commented Jan 5, 2021

I've unified things into a single HPy_CallTupleDict. The API seems nice now, but I'm not sure I'm wild about the amount of custom C code in HPy_CallTupleDict or that it calls back to multiple different C Python API methods.

I think that having CallTupleDict which maps to either Call or CallObject is fine, provided that it doesn't cost any performance on CPython.
It looks like an improvement from the API point of view and it should be very easy for people to port their code. Maybe add also a note to docs/porting-guide.rst? The document is very rough at the moment and we should do it properly, but it's good to start collecting notes while we implement things.

Copy link
Collaborator

@antocuni antocuni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it :), see also my comments above.

Also, maybe the title of the PR should be updated

}
else {
// args is null, but kw is not, so we need to create an empty args tuple
// for CPython's PyObject_Call
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, nice catch. I didn't know that CPython doesn't have a way to pass only kwargs. I don't know if it will be actually useful but it doesn't cost anything to add support for it, so it's probably a good idea to be more complete, +1



class TestCall(HPyTest):
def argument_combinations(self, *items):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea! Maybe write it to take **items instead of *items? It would make the calls much nicer to read, and and I think that on modern Python the order of items in the dict is guaranteed, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is. Let me try that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woot. Implemented. Thank you for the suggestion.

@hodgestar hodgestar changed the title Add HPy_Call, HPy_CallObject, and HPyCallable_Check. Add HPy_CallTupleDict, HPyCallable_Check and HPyTuple_Check. Jan 5, 2021
@hodgestar
Copy link
Contributor Author

Porting guide updated.

@hodgestar
Copy link
Contributor Author

@antocuni Ready for a (hopefully) final review.

Copy link
Collaborator

@antocuni antocuni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I think it's ready to merge!
For the records, I tried to run the microbench on bencher7, here are the results or a single run (I ran it few times and didn't see any noticeable difference):

                                                     cpy                    hpy
                                        ----------------    -------------------
TestModule::test_call_with_tuple              1144.95 us      1146.30 us [1.00]
TestModule::test_call_with_tuple_and_dict     1909.51 us      1884.72 us [0.99]

@hodgestar hodgestar merged commit eb07982 into master Jan 11, 2021
@hodgestar
Copy link
Contributor Author

Thanks for reviewing! Your benchmark numbers look very similar to those on my laptop, where there is also no difference visible across several runs.

This was referenced Nov 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants