Skip to content

Defer string formatting in asyncio Task creation #103793

Description

@itamaro

Feature or enhancement

When an asyncio Task is created without passing in a name (a common case), the init method uses a global counter to generate a task name of the form "Task-<counter>".

It is very common in applications that the task name is never read or used, and string formatting has non-negligible runtime cost, making task creation slower. It would be beneficial to defer the string formatting operation and avoid incurring that overhead during task creation.

This can be done by storing the counter in the task struct, and using it to lazily populate the name in the get_name method.

Linked PRs

Activity

  1. kumaraditya303 commented on Apr 25, 2023

    @kumaraditya303
    Contributor

    I am surprised that string formatting is so slow to make a difference here.

    If we are implementing this optimization then I suggest to use a tagged pointer and a union for task name and counter combined into a single word with LSB set for tagging.

  2. vladima commented on Apr 25, 2023

    @vladima
    Contributor

    This is the way it was originally implemented in Cinder :)

  3. itamaro commented on Apr 25, 2023

    @itamaro
    ContributorAuthor

    so far we have 3 different implementation approaches that provide similar perf benefits:

    1. assign task number lazily in get_name.
      • Pros: no changes to the struct. very simple impl.
      • Cons: changes the behavior of task numbers assignment
    2. use tagged pointer to make the task name field be a union of task name and counter.
      • Pros: doesn't change behavior of task numbers assignment. doesn't add fields to the task struct.
      • Cons: most complex impl.
    3. store the task number in a new field on the task struct to be used for deferred formatting in get_name.
      • Pros: doesn't change behavior of task numbers assignment. relatively simple impl.
      • Cons: adds a field to the task struct.

    @kumaraditya303 iiuc you prefer option 2, for the benefit of not changing the order of task number allocation.
    do you think it is worth the complexity, considering task numbers are mainly used for debugging and logging, and there are no guarantees provided by python about the semantics of the task name if not set by the user?

  4. markshannon commented on Apr 25, 2023

    @markshannon
    Member

    Option 2 seems needlessly complicated for something as relatively unimportant as task names.

    Is there an expectation that the numbers in task names increase over time?
    If not, then option 1 is the best in terms of both performance and memory use

    Option 4 is to assign the number at task creation and compute the name whenever it is requested.
    This costs no additional memory, as it replaces the name field with an index field.

  5. kumaraditya303 commented on Apr 25, 2023

    @kumaraditya303
    Contributor

    Option 4 is to assign the number at task creation and compute the name whenever it is requested.
    This costs no additional memory, as it replaces the name field with an index field.

    How does that work for user provided task names?

  6. itamaro commented on Apr 25, 2023

    @itamaro
    ContributorAuthor

    I would love to go with option 1 :)

    I think option 4 can ~work if we do something like this:

    • in construction - if user-provided string, do what we do today. if no user-provided - assign the number to the name field as a PyLong object
    • in get_name - if the name field is a PyLong, then use it to format the name. otherwise do what we do today.

    if we go with this, we can go ahead and cache the formatted name back to the name field, which makes it similar to option 2 but using type checks instead of tagged pointers.

  7. willingc commented on Apr 26, 2023

    @willingc
    Contributor

    I like @markshannon's option 4 with the your suggestions @itamaro. I think Option 1's Con makes it less attractive to me.

  8. gvanrossum commented on Apr 26, 2023

    @gvanrossum
    Member

    Looking again, I now agree that the order in which task numbers are assigned may feel meaningful to the user, and only assigning the number when the task is being printed could cause some problems. E.g. the user has several tasks, and no matter in which order they print them, the first one printed is always named "Task-1" -- this could be very confusing. So this is another vote for 3 or 4. (I would be fine with 3. From the words Mark used when he proposed 4, I assume he missed that sometimes the user passes in a name which should override this mechanism.)

  9. itamaro commented on Apr 26, 2023

    @itamaro
    ContributorAuthor

    Thanks for the feedback!
    If my interpretation of option 4 is acceptable, I will go with that! Rather not add fields to the struct if not strictly necessary.

  10. moved this from Todo to Done in asyncioon Apr 29, 2023
  11. added a commit that references this issue on Apr 29, 2023
  12. gvanrossum commented on Apr 29, 2023

    @gvanrossum
    Member

    Thanks for this improvement! (Would still like to see the perf numbers now that we allocate a PyLong during init.)

  13. added a commit that references this issue on May 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions