Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ time
a resolution of 1 millisecond (10\ :sup:`-3` seconds).
(Contributed by Benjamin Szőke and Victor Stinner in :issue:`21302`.)

typing
------

* :func:`typing.cast` now has C implementation. It is now around 2 times faster
to call it.
(Contributed by Yurii Karabas in :issue:`44775`.)

unicodedata
-----------

Expand Down Expand Up @@ -480,6 +487,10 @@ Changes in the Python API
the ``'utf-8'`` encoding.
(Contributed by Srinivas Reddy Thatiparthy (శ్రీనివాస్ రెడ్డి తాటిపర్తి) in :issue:`41137`.)

* :func:`typing.cast` no longer accepts keyword arguments. Note that static
checkers have always rejected this. Now it's also enforced at runtime.
(Contributed by Yurii Karabas in :issue:`44775`.)

* When sorting using tuples as keys, the order of the result may differ
from earlier releases if the tuple elements don't define a total
ordering (see :ref:`expressions-value-comparisons` for
Expand Down
37 changes: 34 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
import typing
import weakref
import types
import pydoc

from test.support import import_helper
from test.support import captured_stdout, import_helper
from test import mod_generics_cache
from test import _typed_dict_helper

Expand Down Expand Up @@ -2539,9 +2540,10 @@ def func(x): ...
self.assertIs(func, final(func))


class CastTests(BaseTestCase):

class CastTests:
def test_basics(self):
cast = self.module.cast

self.assertEqual(cast(int, 42), 42)
self.assertEqual(cast(float, 42), 42)
self.assertIs(type(cast(float, 42)), int)
Expand All @@ -2552,10 +2554,39 @@ def test_basics(self):
self.assertEqual(cast(None, 42), 42)

def test_errors(self):
cast = self.module.cast

# Bogus calls are not expected to fail.
cast(42, 42)
cast('hello', 42)

def test_keyword_arguments(self):
with self.assertRaises(TypeError):
self.module.cast(type=int, val=10)

with self.assertRaises(TypeError):
self.module.cast(int, val=10)


class CastPythonTests(CastTests, BaseTestCase):
module = py_typing


@skipUnless(c_typing, 'requires _typing')
class CastCTests(CastTests, BaseTestCase):
module = c_typing

def test_help(self):
def _get_doc(func):
with captured_stdout() as stdout:
pydoc.help(func)

# skip first line because it different for regular and built-in
_, doc = stdout.getvalue().split("\n", 1)
return doc

self.assertEqual(_get_doc(c_typing.cast), _get_doc(py_typing.cast))


class ForwardRefTests(BaseTestCase):

Expand Down
20 changes: 11 additions & 9 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1714,16 +1714,18 @@ def close(self): ...
cls._is_runtime_protocol = True
return cls

try:
from _typing import cast
except ImportError:
def cast(typ, val, /):
"""Cast a value to a type.

def cast(typ, val):
"""Cast a value to a type.

This returns the value unchanged. To the type checker this
Comment thread
uriyyo marked this conversation as resolved.
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val
This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val


def _get_defaults(func):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Speed-up ``typing.cast`` by implementing it in C. Also disallow passing keyword
arguments to ``typing.cast``. Patch provided by Yurii Karabas.
24 changes: 24 additions & 0 deletions Modules/_typingmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,33 @@ _typing__idfunc(PyObject *module, PyObject *x)
return x;
}

/*[clinic input]
_typing.cast -> object

typ: object
val: object

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you make the value positional only this couldn't probably be even faster

@uriyyo uriyyo Jul 30, 2021

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree it will make cast faster, but in such case it won't be compatible with current version of typing.cast that can accept arguments as keywords.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Um, does mypy even accept keyword args? I don't see any reason why anyone should use keyword args for cast().

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay then I think the concern is moot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we drop keyword support and make this only METH_FASTCALL, the specializer may eventually make this 8% faster (it currently only does so for CALL_FUNCTION, not CALL_METHOD).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@uriyyo could you consider removing keyword support please?

For consistency, we can also make the Python typing.cast version keyword-only (we will need to add an entry in What's New and news for that later since it breaks invalid code).

/

Cast a value to a type.

This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
[clinic start generated code]*/

static PyObject *
_typing_cast_impl(PyObject *module, PyObject *typ, PyObject *val)
/*[clinic end generated code: output=11224a3fa037a9a1 input=bde696783400a5b0]*/
{
Py_INCREF(val);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am a bit concerned about this new module where all the functions do the same thing (just return the same value). We should speed up the calls in general as implementing these trivial functions in c to avoid the call overhead feels like a maintainance concern to me.

To be clear, in not opposed to this pr but it starts to feel like a anti-pattern smell.

@corona10 corona10 Jul 30, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have the same concern with @pablogsal and I would like to request to typing modules authors to follow conventional CPython accelerated extension module structure which is written based on class, function unit not the partial instance method, etc. (This case looks proper as module function unit)

Those extension modules maintain Python and C versions even though maintain cost exists.
And those decisions are based on that the accelerated gain is worth than maintain cost.
So If we feel that following conventional structure is too complicated as the maintenance view,
IMHO, it means that the accelerated version is not worth writing.
For my example, I love to implement vectorcall but I decided to drop my enumerate vectorcall because the implementation was too complicated. (see #25154)

cc @Fidget-Spinner

@corona10 corona10 Jul 30, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Anyway, this PR makes 2x faster on my local machine.

Mean +- std dev: [cast_base] 114 ns +- 5 ns -> [cast_pr] 56.0 ns +- 1.8 ns: 2.03x faster
import pyperf

runner = pyperf.Runner()
runner.timeit(name='bench typing.cast',
              stmt='val = typing.cast(int, num)',
              setup = '''
import typing
import random
num = random.randint(0, 10000)
''')

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@pablogsal @corona10 Thanks for your opinion, and I agree with your points.

Case of typing.cast is super simple like it was with typing.NewType.__call__ and we can get x2 boost with small efforts and in my opinion it will be great to have such boost)

I understand your concerns regarding maintenance costs and it's valid point for me.

@Fidget-Spinner Fidget-Spinner Jul 30, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And those decisions are based on that the accelerated gain is worth than maintain cost.

@corona10 thank for pinging me. To be honest I don't know enough to comment. Properly benchmarking typing is very hard and still unsolved. IMO, there are 3 aspects to typing performance:

  1. Static type checkers implemented in Python like mypy.
  2. Runtime type checkers/introspection like Pydantic.
  3. Runtime overhead of typed code vs fully untyped code.

All 3 will benefit from general CPython optimizations (eg. Vectorcall, specialization, cache). Although only 3. will benefit the most from speedups to typing module. 2. will benefit a little. 1. will have little benefit.

I'm working on covering these cases in pyperformance (I already submitted 1 open PR for case 1.). And I will send out an email to typing-sig for discussion on 2. and 3. soon.

To be clear, I am not against this PR at all (I'm +0). I just don't have enough data at the moment to decide :(.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The entire reason _typing exists is that the identity function it houses was deemed improper to put inside _functools or _operator. I think the identity functions have a place as they are next to trivial development-wise but provide a significant performance improvement.

I understand @corona10's argument about providing a full symmetrical accelerated C module vs. a Python module but this currently neither necessary (because it's just two identity functions) nor particularly feasible (because it's a lot of duplicate work and typing is still evolving quite rapidly).

IMO it would be best just to take this improvement as is.

@pablogsal pablogsal Jul 30, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

but this currently neither necessary (because it's just two identity functions) nor particularly feasible (because it's a lot of duplicate work and typing is still evolving quite rapidly).

That is necessary per PEP399, as these functions are going to be the public API

@pablogsal pablogsal Jul 30, 2021

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In particular:

This PEP requires that in these instances that the C code must pass the test suite used for the pure Python code to act as much as a drop-in replacement as reasonably possible.

Given that we are seeking speed, we could drop a bit the requirement and use positional-only, but we are on the line of the interpretation of PEP 399

return val;
}


static PyMethodDef typing_methods[] = {
_TYPING__IDFUNC_METHODDEF
_TYPING_CAST_METHODDEF
{NULL, NULL, 0, NULL}
};

Expand Down
37 changes: 36 additions & 1 deletion Modules/clinic/_typingmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.