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

GH-128939: Refactor JIT optimize structs #128940

Merged
merged 7 commits into from
Jan 20, 2025

Conversation

markshannon
Copy link
Member

@markshannon markshannon commented Jan 17, 2025

Converts the ad-hoc set of flags in _Py_UopsSymbol into a tagged union.
This:

  • Makes the code more robust. Using an enum in a switch allows the C compiler to tell us if we miss any cases
  • More extensible. New kinds can be added by adding a new tag type
  • Easier to maintain. There are no (at least much fewer) hidden assumptions in the data
  • Renames all structs starting to _Py_Uops to start with JitOpt.
  • Adds a new tuple kind to demonstrate that it can be done easily and reliably.

@Fidget-Spinner
Copy link
Member

Can you add a test that the tuple information is actually propagated? Either through testing type guard elimination happens or something. Unless you plan to refactor the tests as well :).

@brandtbucher
Copy link
Member

Random comment: JIT is an acronym, and we use the "JIT" capitalization elsewhere. "Jit" just looks... wrong.

Copy link
Member

@brandtbucher brandtbucher left a comment

Choose a reason for hiding this comment

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

Various thoughts and questions I had while reading through. I think this is going to be a very nice improvement, though!

Other thoughts:

  • It's probably worth adding a helper for immortality checks. Basically, check if we have an immortal constant, or a known bool type. The bool part is nice, but easy to forget.
  • Any plans to update the _Py_uop_ naming to be "JIT"-ier?

Comment on lines +225 to +231
typedef union _jit_opt_symbol {
uint8_t tag;
JitOptKnownClass cls;
JitOptKnownValue value;
JitOptKnownVersion version;
JitOptTuple tuple;
} JitOptSymbol;
Copy link
Member

Choose a reason for hiding this comment

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

Could this be:

Suggested change
typedef union _jit_opt_symbol {
uint8_t tag;
JitOptKnownClass cls;
JitOptKnownValue value;
JitOptKnownVersion version;
JitOptTuple tuple;
} JitOptSymbol;
typedef struct {
uint8_t tag;
union {
JitOptKnownClass cls;
JitOptKnownValue value;
JitOptKnownVersion version;
JitOptTuple tuple;
};
} JitOptSymbol;

Then we wouldn't need to repeat the tag in each struct.

Or does the current scheme potentially pack better?

Copy link
Member Author

Choose a reason for hiding this comment

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

It packs better. Because structs align according to the alignment of their members, we would end up using 8 bytes for the tag if it weren't part of each struct.

Comment on lines +203 to +207
typedef struct _jit_opt_known_class {
uint8_t tag;
uint32_t version;
PyTypeObject *type;
} JitOptKnownClass;
Copy link
Member

Choose a reason for hiding this comment

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

We could probably just get the version by doing cls.type->tp_version_tag, right? Having both here just creates opportunities for them to be stale or out-of-sync, I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

sym->cls.type->tp_version_tag is the version tag of the value's class now. sym->version.version is the version tag it would have should execution reach this point in the code. Not quite the same thing.

Copy link
Member

@brandtbucher brandtbucher Jan 22, 2025

Choose a reason for hiding this comment

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

I would imagine that a sym->cls.version that's out-of-sync with sym->cls.type->tp_version_tag should result in the bottom type, then, right? It's not possible in future code, since the cached tag is stale.

typedef struct _jit_opt_tuple {
uint8_t tag;
uint8_t length;
uint16_t items[6];
Copy link
Member

Choose a reason for hiding this comment

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

I think we have room for 7, right?

Suggested change
uint16_t items[6];
uint16_t items[7];

Comment on lines +176 to +182
case JIT_SYM_KNOWN_VALUE_TAG:
Py_CLEAR(sym->value.value);
sym_set_bottom(ctx, sym);
return false;
case JIT_SYM_TUPLE_TAG:
sym_set_bottom(ctx, sym);
return false;
Copy link
Member

Choose a reason for hiding this comment

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

These could potentially have the same version tag, right? Looking up a method or whatever on a constant value is a normal thing to do.

Comment on lines +227 to +229
case JIT_SYM_TUPLE_TAG:
sym_set_bottom(ctx, sym);
return;
Copy link
Member

@brandtbucher brandtbucher Jan 18, 2025

Choose a reason for hiding this comment

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

This also seems like it could potentially be fine (transitioning from an abstract tuple to a concrete one)... could potentially need to do something recursive for the elements, though.

_Py_UopsSymbol *
_Py_uop_sym_new_unknown(_Py_UOpsContext *ctx)
JitOptSymbol *
_Py_uop_sym_new_unknown(JitOptContext *ctx)
{
return sym_new(ctx);
Copy link
Member

Choose a reason for hiding this comment

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

Should this be checked for NULL and replaced with out_of_space like the others?

_Py_uop_sym_tuple_getitem(JitOptContext *ctx, JitOptSymbol *sym, int item)
{

if (sym->tag != JIT_SYM_TUPLE_TAG || item < 0 || item >= sym->tuple.length) {
Copy link
Member

Choose a reason for hiding this comment

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

We can handle negative indexes pretty easily here, right?

Also, this can probably be updated to work correctly for constant tuples.

Copy link
Member Author

Choose a reason for hiding this comment

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

Negative indexes are errors at this level. If we add a symbolic version of this, we would handle negative indexes there.

int
_Py_uop_sym_tuple_length(JitOptSymbol *sym)
{
if (sym->tag != JIT_SYM_TUPLE_TAG) {
Copy link
Member

Choose a reason for hiding this comment

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

Same here, this can probably be updated to work for constants.

Comment on lines 447 to 450
if (size > 6) {
res->tag = JIT_SYM_KNOWN_CLASS_TAG;
res->cls.type = &PyTuple_Type;
}
Copy link
Member

Choose a reason for hiding this comment

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

There are probably cases where this can be represented as a constant value (when all items are known).

Copy link
Member

Choose a reason for hiding this comment

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

Hm, although it could get tricky because nothing owns that constant, so we have to be careful not to promote its uses to constant loads.

Copy link
Member Author

Choose a reason for hiding this comment

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

A constant tuple holds no more information than a tuple of constants, so there would be no advantage.

Copy link
Member

Choose a reason for hiding this comment

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

Well, it can have more than 7 items... but sure, that probably isn't very common.

@markshannon
Copy link
Member Author

Random comment: JIT is an acronym, and we use the "JIT" capitalization elsewhere. "Jit" just looks... wrong.

Readability. Is JITShow "JIT show" or "JITs how"?

@markshannon
Copy link
Member Author

Any plans to update the Py_uop naming to be "JIT"-ier?

At some point, yes.

self.assertIn("_BINARY_OP_ADD_INT", uops)
self.assertNotIn("_GUARD_BOTH_INT", uops)
self.assertNotIn("_GUARD_NOS_INT", uops)
self.assertNotIn("_GUARD_TOS_INT", uops)
Copy link
Member

Choose a reason for hiding this comment

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

This kind of check can easily pass by accident if there's typo or bytecodes change.

Would be good to have a helper that asserts also that the thing not in is an actual uop id.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would.
I'll do that in another PR though.

@markshannon markshannon merged commit f0f7b97 into python:main Jan 20, 2025
64 checks passed
srinivasreddy pushed a commit to srinivasreddy/cpython that referenced this pull request Jan 21, 2025
@brandtbucher
Copy link
Member

Random comment: JIT is an acronym, and we use the "JIT" capitalization elsewhere. "Jit" just looks... wrong.

Readability. Is JITShow "JIT show" or "JITs how"?

I don't know, "JIT show" seems like the pretty obvious CamelCase reading. Similar to how "GILState" is "GIL State" and not "GILs tate". Not a hill I'm willing to die on, it just looks kinda weird (like "Ast" or "Ascii" or "Gil" would).

@markshannon markshannon deleted the refactor-optimize-structs branch January 31, 2025 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants