On 18 Jun 2020, at 18:37, Christopher Barker <pythonchb@gmail.com> wrote:

On Thu, Jun 18, 2020 at 9:34 AM Barry Scott <barry@barrys-emacs.org> wrote:
To make the code avoid COW you would need to be able to make sure that all code memory blocks are not mixed in with PyObject memory blocks.

Then the ref count dance will have trigger COW for the code.

indeed. cPython already has its own memory allocator, yes? how hard would it be to allocate all "immortal" objects in the same region in memory, and regular objects in another? Presumably that would only be a cost at allocation time, and probably not a large one. So the trick is to determine what objects are immortal.

And Jonathan has a good point: though Python is perfectly capable of creating and destroying code objects (functions, classes, etc, in practice, most do survive the length of the program, so there would be little memory wasted in most cases by making them immortal. And maybe the interpreter could be smart about guessing which are most likely to be mortal. Finally, if this really does make a difference, then we could add ways for the programmer to mark certain code objects as mortal or immortal as need be.

The key part of the idea is that the memory holding the ref count is not adjacent to the memory holding the objects state.
Further that rarely modified state should be kept away from usually modified state.

PyObject in the ref-count heap.
Code objects in rarely-modified heap.
List and Dict etc in the usually-modified heap.

I'm assuming that its on longer true that  a PyObject header is prepended to the memory of an objects state.

Something like this has been described on I think the CAPI mailing list in some form.

Barry




Finally -- and I'm way out of my depth here -- does this mean there is potential to significantly improve the performance of multiprocessing? Which would be really, really, great, as the GIL has proven an intractable barrier to certain kinds of multi-threading.

-CHB










 
Barry


I hope this helps.

Jonathan

APPENDICES
===========

SOME IMPLEMENTATION DETAILS AND COMMENTS
Because fn.__code__ must not return a permanent object, some sort of opaque proxy would be required. Because Python programs rarely inspect fn.__code__, in practice the cost of this additional indirection is likely to be small.

As things are, the time spent changing the refcount of fn.__code__ is probably insignificant. The benefit is that permanent code objects are made immutable, and so can be stored safely in read-only memory (that can be shared across all processes and users). Code objects are special, in that they are only rarely looked at directly. Their main purpose is to be used by the interpreter.

Python allows the user to replace fn.__code__ by a different code object. This is a rarely done dirty trick. The transient / permanent nature of fn.__code__ could be stored as a hidden field on the fn object. This would reduce the cost of the if ... else ... branching, as it amounts to caching the transient / permanent nature of fn.__code__.

FORK AND COPY ON WRITE
On Unix, the fork system call causes a process to make a child of itself. The parent and child share memory. To avoid confusion and errors, when either asks the system to write to shared memory, the system ensures that both parent and child have their own copy (of the page of memory that is being written to). This is an expensive operation.

INTERPRETER SESSION

    >>> from sys import getrefcount as grc

    # Identical functions with different code objects.
    >>> def f1(obj): return grc(obj)
    >>> def f2(obj): return grc(obj)
    >>> f1.__code__ is f2.__code__
    False

    # Initial values.
    >>> grc(f1.__code__), grc(f2.__code__)
    (2, 2)

    # Calling f1 increases the refcount of f1.__code__.
    >>> f1(f1), f1(f2), f2(f1), f2(f2)
    (6, 4, 4, 6)

    # If fn is a generator function, then x = fn() will increase the
    # refcount of fn.__code__.
    >>> def f1(): yield True
    >>> grc(f1.__code__)
    2

    # Let's create and store 10 generators.
    >>> iterables = [f1() for i in range(10)]
    >>> grc(f1.__code__)
    22

    # Let's get one item from each.
    >>> [next(i) for i in iterables]
    [True, True, True, True, True, True, True, True, True, True]
    >>> grc(f1.__code__)
    22

    # Let's exhaust all the iterables. This reduces the refcount.
    >>> [next(i, False) for i in iterables]
    [False, False, False, False, False, False, False, False, False, False]
    >>> grc(f1.__code__)
    12

    # Nearly done. Now let go of the iterables.
    >>> del iterables
    >>> grc(f1.__code__)
    2



_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-leave@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/SFCJS2UDC25LFZXMVJMOZ75VNCVHQUDJ/
Code of Conduct: http://python.org/psf/codeofconduct/

_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-leave@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/TZO7AGYR35XP6CRKPWAMZ2KW2OB6VBGL/
Code of Conduct: http://python.org/psf/codeofconduct/


-- 
Christopher Barker, PhD

Python Language Consulting
  - Teaching
  - Scientific Software Development
  - Desktop GUI and Web Development
  - wxPython, numpy, scipy, Cython