-
-
Notifications
You must be signed in to change notification settings - Fork 3k
[mypyc] feat: new primitive for int.to_bytes
#19674
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
base: master
Are you sure you want to change the base?
Conversation
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I will keep this open for a day or two in case @JukkaL has some comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the second look I think I have some more questions.
mypyc/lib-rt/int_ops.c
Outdated
// int.to_bytes(length, byteorder, signed=False) | ||
PyObject *CPyTagged_ToBytes(CPyTagged self, Py_ssize_t length, PyObject *byteorder, int signed_flag) { | ||
PyObject *pyint = CPyTagged_StealAsObject(self); | ||
if (!PyLong_Check(pyint)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the second thought, all these type checks look unnecessary, normally Python wrappers should do them. You can probably verify this by adding some run
tests with Any
s in them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what, like this?
def f(x: Any) -> bytes:
return int.to_bytes(x)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think CPyTagged_StealAsObject
is not correct there, since it will transfer the ownership of the parameter, and this can cause a double free. CPyTagged_AsObject
will return a new reference which you can decref at the end of the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what, like this?
First, not just the self
, second I think you should try more something like this
def to_bytes(n: int, length: int, byteorder: str = "little", signed: bool = False) -> bytes:
return n.to_bytes(length, byteorder, signed=signed)
x: Any = "no"
bad: Any = "way"
to_bytes(x, bad)
and check that a TypeError
will be given even before getting to your specialized code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could implement this test, but wouldn't we then just be testing the standard python-wrapper type validation functionality, as opposed to some specific functionality related to this PR?
I can still add the tests accordingly, I just want to make sure we have the same understanding of things before I proceed.
assert to_bytes(255, 2, "big") == b'\x00\xff' | ||
assert to_bytes(255, 2, "little") == b'\xff\x00' | ||
assert to_bytes(-1, 2, "big", True) == b'\xff\xff' | ||
assert to_bytes(0, 1, "big") == b'\x00' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also test calling to_bytes()
function from interpreted code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ilevkivskyi how would I implement that? Is there a good example I can look from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I measured the performance impact using a micro-benchmark, and the performance was pretty similar to master on Python 3.13:
import time
def bench(n: int) -> None:
for i in range(n):
i.to_bytes(4, "little", signed=True)
bench(10 * 1000 * 1000)
t0 = time.time()
bench(30 * 1000 * 1000)
print(time.time() - t0)
It's possible that the operation is expensive enough that the call overhead when there's no primitive isn't significant. In any case, since maintaining a new primitive takes some effort, it's important that there's some measurable performance improvement first. Have you been able to measure a performance improvement?
what if you assign the result to a value? That way we can include the interpreted version's unboxing penalty in our benchmark.
|
Separately, I might be able to slightly optimize the little/big check by only checking the length of the input string once, since we already know the length of 'little' and 'big'. Or alternatively, I could make a bespoke version for each of the two cases and eliminate the check entirely. This would be fastest but I'm not sure if there's currently a clean way to implement the method_op |
Having a specializer function that choosen a little/big endian variant of the primitive if the argument is a string literal could help (in |
That's odd. I fixed the kw-only arg in to_bytes stub, but doing so breaks the IR. Is there a way for me to cover this case with a method_op? |
1.18 vs this branch There was a ~5-8% increase in speed for various permutations of the int_to_big_endian benchmark which includes not only a call to to_bytes but a decent bit of other stuff as well |
This PR adds a new primitive for all arg combinations of
int.to_bytes