Skip to content

bpo-43693: Add _PyCode_New() and do some related cleanup. #26258

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

Closed

Conversation

ericsnowcurrently
Copy link
Member

@ericsnowcurrently ericsnowcurrently commented May 19, 2021

The key change here is the addition of _PyCode_New(), which is an internal-only API to replace use of PyCode_NewWithPosOnlyArgs(). The problem with that older API is the it has so many parameters. This becomes especially painful when modifying its signature. The new API uses a single struct to hold the values needed to create a new PyCodeObject instance.

Other changes:

  • added co_nfastlocals, co_ncellvars, and co_nfreevars; this matters because I plan on replacing co_varnames, etc. with a single consolidated tuple
  • moved some of the code tied to the internal resolution of co_varnames, etc. from ceval.c to codeobject.c (as functions); this reduced coupling makes it easier to change how PyCodeObject works internally
  • re-arranged the PyCodeObject fields so they are grouped a bit more logically

There is still code in ceval.c (_PyEval_MakeFrameVector() specifically, and pyframeobject.c that is coupled to the internal structure of PyCodeObject, but this PR is big enough already. 🙂

https://bugs.python.org/issue43693

@markshannon
Copy link
Member

I agree that we should add a non-API function for creating code objects and _PyCodeNew seems like a sensible name for it.
But surely that should be a 10 line diff, not a 1000 line diff?

Passing a struct as a single parameter to a function means that you have to ensure that all future versions can handling trailing zeroes.
E.g.

PyObject *make_thing(struct thing *t);

Version 1:

struct thing {
    some data;
};

Version 2:

struct thing {
   some data;
   int super_important_must_never_be_zero;

Any client code written for version 1 will compile for version 2 and then crash mysteriously when run.

@ericsnowcurrently
Copy link
Member Author

I see your point but consider it less of an issue for an internal-only API like this. For the most part this PR is about addressing the pain points I ran into while making changes around PyCodeObject:

  • changes to the public "constructor" API to accommodate changes to internal details are frustrating for everyone (hence the new internal function)
  • much of the public API for PyCodeObject probably shouldn't be public in the first place
  • the signature of 2 of the "constructor" functions are particularly large, so adding, removing, or re-arranging parameters is annoying (hence the args struct, which we also do for frames)
  • for folks not already familiar with PyCodeObject, the nature of the various fields isn't obvious at first (hence grouping the fields)
  • more than a couple modules are fairly tightly coupled to the internal details of PyCodeObject, which may make sense for ceval.c as the confluence of functions, code, and frames, but that coupling (even in ceval,c) means changes to PyCodeObject internals result in touches to a variety of other modules (and the number of modules touched for a change correlates to bug count)

I'm okay with dialing back the changes if you think they aren't worth it, but the definitely are based in my experience thus far with PyCodeObject.

@markshannon
Copy link
Member

I think we all agree that the API for making PyCodeObjects shouldn't have been public in the first place. But it is 😞
I'm in favour of removing it, but that would need a proper discussion of python-dev.

The accessor macros, _PyCode_GET_INSTRUCTION etc, seem useful, but could you make them inline functions.

Ultimately we want something like https://github.com/markshannon/peps/blob/pep-mappable-pyc-file/pep-066x.rst#runtime-objects
It is going to take a while to get there, but let's try and minimize the changes along the way.

@markshannon
Copy link
Member

Please remove construction of a CodeObject using a structure. As I explained earlier it isn't safe.
Nor does it do anything for clarity, IMO, it just adds bulk.

Copy link

@bl-ue bl-ue left a comment

Choose a reason for hiding this comment

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

Hi, I found a few things worth taking a look at :)

@gvanrossum
Copy link
Member

Please remove construction of a CodeObject using a structure. As I explained earlier it isn't safe.

That only applies for public APIs. This is a purely internal API. The compiler will catch the case you worry about in that explanation.

Nor does it do anything for clarity, IMO, it just adds bulk.

It allows us something not entirely unlike keyword arguments in C, which I find a big gain to readability.

@markshannon
Copy link
Member

No, the compiler won't catch it. It is legal to leave trailing fields off a struct assignment.
It only works if NULL or 0 is a sensible value for all the fields. For this struct, that is not the case. Even if it were, we would have laid a trap for future developers adding new fields.

@ericsnowcurrently
Copy link
Member Author

@markshannon, calling check_code() will catch the problem you described. That said, I don't see this as a problem for this internal API. As to readability. It's what Guido said. To elaborate,: the order doesn't matter and you can leave "args" off if you want to use the default.

Regardless, there isn't a lot of value to a long discussion about this. If you feel strongly about this then I'll change it to a function with many parameters.

Copy link
Member

@markshannon markshannon left a comment

Choose a reason for hiding this comment

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

This PR is too big for me to be confident that I'm not missing something important.
Why does it need to be so large?

There are a lot of changes to frameobject.c, typeobject.c and ceval.c that seem unrelated.
Several of which look like they will degrade performance.

PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */

Copy link
Member

Choose a reason for hiding this comment

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

co_code, co_consts and co_names are the hottest fields in the code object. Could make sure that they are near the start of the object. Moving the metadata section after this section should do.

@@ -24,9 +27,147 @@ struct _PyOpcache {
char optimized;
};


// We would use an enum if C let us specify the storage type.
Copy link
Member

Choose a reason for hiding this comment

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

You can still use an enum to define the values though, even if you store them in a char.

}

static inline bool
_PyCode_CodeIsValid(PyCodeObject *co)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't check for validity, at all. A full validity check would be large and slow. Maybe rename to reflect what it does?

}

static inline _Py_CODEUNIT *
_PyCode_GetInstructions(PyCodeObject *co)
Copy link
Member

Choose a reason for hiding this comment

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

This is going to be misleading with quickening, so could you drop it for now.
Plus these accessor functions have an annoying tendency to end up in the public API, historically.

PyObject *filename, PyObject *name, int firstlineno,
PyObject *linetable, PyObject *exceptiontable)
static int
check_code(struct _PyCodeConstructor *con, enum check_mode mode)
Copy link
Member

Choose a reason for hiding this comment

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

Using boolean parameters (and check_mode is a boolean) that change behavior is generally regarded as bad API design.
I think it would be better if the legacy form just cleared the exception then called PyErr_BadInternalCall()

@@ -649,6 +649,9 @@ frame_dealloc(PyFrameObject *f)
static inline Py_ssize_t
frame_nslots(PyFrameObject *frame)
{
assert(frame->f_valuestack - frame->f_localsptr ==
// fastlocals + builtins + globals + locals
frame->f_code->co_nfastlocals + 3);
Copy link
Member

Choose a reason for hiding this comment

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

OK, I see it is just misnamed, and the comment is misleading. Cell and free variables are also included.

@@ -917,146 +920,28 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code,
return f;
}

/* Convert between "fast" version of locals and dictionary version.
Copy link
Member

Choose a reason for hiding this comment

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

Why all these changes? This seems unrelated to adding _PyCode_New().

@@ -8830,71 +8831,50 @@ static int
super_init_without_args(PyFrameObject *f, PyCodeObject *co,
PyTypeObject **type_p, PyObject **obj_p)
{
if (co->co_argcount == 0) {
if (!_PyCode_HasFastlocals(co, CO_FAST_POSONLY | CO_FAST_POSORKW)) {
Copy link
Member

Choose a reason for hiding this comment

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

Why? This is performance critical code, and this looks a lot slower.

}

int
_PyCode_OffsetFromIndex(PyCodeObject *co, int index, _PyFastLocalKind kind)
Copy link
Member

Choose a reason for hiding this comment

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

I don't like the use of this function everywhere.
If inlined, it should be no slower than the code it is replacing, but what is the point?
index + co->co_nlocals is clearer to me than _PyCode_OffsetFromIndex(co, index, CO_FAST_CELL).
index is far, far clearer than _PyCode_OffsetFromIndex(co, index, CO_FAST_LOCAL)`

if (co->co_flags & CO_VARKEYWORDS) {
kwdict = PyDict_New();
if (kwdict == NULL)
goto fail;
i = total_args;
int offset = _PyCode_OffsetFromIndex(co, total_args, CO_FAST_LOCAL);
Copy link
Member

Choose a reason for hiding this comment

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

This is performance critical code and this is going to be slower unless _PyCode_OffsetFromIndex is inlined.

@bedevere-bot
Copy link

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

@ericsnowcurrently
Copy link
Member Author

If this PR is too big to review well enough then I'll split it up.

@ericsnowcurrently
Copy link
Member Author

I'm closing this in favor of #26364 and #26375. There are more changes than those cover, but the rest can be dealt with (or abandoned) later.

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.

6 participants