Skip to content

Remove unnecessary loads "critical_section" from _PyCriticalSection_SuspendAll #158235

Description

@ImUserDol

Compilers were unable to prove that the value at this address will not change, and they do not keep the previously loaded value live across the call; as a result, they reload it both in the or rmw operation and when checking two mutex.
To avoid reloading the value multiple times, we store it once in a register; this prevents the compilers from performing unnecessary load operations, allowing them to simply read the value and perform the OR operation on it. Of course, this puts additional pressure on the registers (in this case, callee-saved), but since they are callee-saved anyway, calls to other functions would save our registers regardless, we simply store our cached critical_section value in them; additionally, the extra push operations shouldn’t be a problem, since they likely fit into the same cache line, so when flushing values, this can be a single cache line flush. Also, here we only pay the cost of the extra registers once, at the beginning of the function. The previous version, on the other hand, re-reads the values each time, so the number of load operations is proportional to the number of critical_sections we iterate through.

Configured with:

    ../configure CC=clang CXX=clang++ \
        --disable-gil --enable-optimizations

Built with:

    make

before:

void
_PyCriticalSection_SuspendAll(PyThreadState *tstate)
{
#ifdef Py_GIL_DISABLED
    uintptr_t *tagptr = &tstate->critical_section;
    while (_PyCriticalSection_IsActive(*tagptr)) {
        PyCriticalSection *c = untag_critical_section(*tagptr);

        if (c->_cs_mutex) {
            PyMutex_Unlock(c->_cs_mutex);
            if ((*tagptr & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
                PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
                if (c2->_cs_mutex2) {
                    PyMutex_Unlock(c2->_cs_mutex2);
                }
            }
        }

        *tagptr |= _Py_CRITICAL_SECTION_INACTIVE;
        tagptr = &c->_cs_prev;
    }
#endif
}

x86_64
clang version 22.1.8
gcc (GCC) 16.2.1 have same problem.

before:

critical_section.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000150 <_PyCriticalSection_SuspendAll>:
 150:	55                   	push   rbp
 151:	41 56                	push   r14
 153:	53                   	push   rbx
 154:	48 8b 9f b0 00 00 00 	mov    rbx,QWORD PTR [rdi+0xb0]
 15b:	48 85 db             	test   rbx,rbx
 15e:	74 40                	je     1a0 <_PyCriticalSection_SuspendAll+0x50>
 160:	89 d8                	mov    eax,ebx
 162:	83 e0 01             	and    eax,0x1
 165:	75 39                	jne    1a0 <_PyCriticalSection_SuspendAll+0x50>
 167:	48 83 e3 fc          	and    rbx,0xfffffffffffffffc // We set 2 bits of the original `critical_section` to zero
...
 178:	f0 0f b0 11          	lock cmpxchg BYTE PTR [rcx],dl
 17c:	75 61                	jne    1df <_PyCriticalSection_SuspendAll+0x8f>
 17e:	f6 87 b0 00 00 00 02 	test   BYTE PTR [rdi+0xb0],0x2 // Since we lost the bottom 2 bits of `critical_section*`, we perform another load to check the value in bit 1
 185:	75 6f                	jne    1f6 <_PyCriticalSection_SuspendAll+0xa6>
 187:	80 8f b0 00 00 00 01 	or     BYTE PTR [rdi+0xb0],0x1 // We're spending uops on a load to perform an OR operation, when we could have stored the value earlier in a register—say, r8.
 18e:	48 8b 03             	mov    rax,QWORD PTR [rbx]
...
 1a4:	c3                   	ret
 1a5:	31 ed                	xor    ebp,ebp
 1a7:	49 89 c6             	mov    r14,rax
 1aa:	49 83 e6 fc          	and    r14,0xfffffffffffffffc
 1ae:	49 8b 7e 08          	mov    rdi,QWORD PTR [r14+0x8]
 1b2:	48 85 ff             	test   rdi,rdi
 1b5:	74 0e                	je     1c5 <_PyCriticalSection_SuspendAll+0x75>
 1b7:	b0 01                	mov    al,0x1
 1b9:	f0 40 0f b0 2f       	lock cmpxchg BYTE PTR [rdi],bpl
 1be:	75 2f                	jne    1ef <_PyCriticalSection_SuspendAll+0x9f>
 1c0:	f6 03 02             	test   BYTE PTR [rbx],0x2 // load
 1c3:	75 5b                	jne    220 <_PyCriticalSection_SuspendAll+0xd0>
 1c5:	80 0b 01             	or     BYTE PTR [rbx],0x1 // load
...

after:

void
_PyCriticalSection_SuspendAll(PyThreadState *tstate)
{
#ifdef Py_GIL_DISABLED
    uintptr_t* tagptr = &tstate->critical_section;
    uintptr_t critical_section = *tagptr;
    while (_PyCriticalSection_IsActive(critical_section)) {
        PyCriticalSection *c = untag_critical_section(critical_section);

        if (c->_cs_mutex) {
            PyMutex_Unlock(c->_cs_mutex);
            if ((critical_section & _Py_CRITICAL_SECTION_TWO_MUTEXES)) {
                PyCriticalSection2 *c2 = (PyCriticalSection2 *)c;
                if (c2->_cs_mutex2) {
                    PyMutex_Unlock(c2->_cs_mutex2);
                }
            }
        }

        *tagptr = critical_section | _Py_CRITICAL_SECTION_INACTIVE;
        tagptr = &c->_cs_prev;
        critical_section = c->_cs_prev;
    }
#endif
}

after:

critical_section.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000150 <_PyCriticalSection_SuspendAll>:
 150:	push   rbp
 151:	push   r15
 153:	push   r14
 155:	push   rbx
 156:	push   rax
 157:	mov    r14,QWORD PTR [rdi+0xb0]
 15e:	test   r14,r14
 161:	je     1ad <_PyCriticalSection_SuspendAll+0x5d>
 163:	mov    eax,r14d
 166:	and    eax,0x1
 169:	jne    1ad <_PyCriticalSection_SuspendAll+0x5d>
 16b:	mov    rbx,r14
 16e:	and    rbx,0xfffffffffffffffc
 172:	mov    rcx,QWORD PTR [rbx+0x8]
 176:	test   rcx,rcx
 179:	je     18f <_PyCriticalSection_SuspendAll+0x3f>
 17b:	xor    edx,edx
 17d:	mov    al,0x1
 17f:	lock cmpxchg BYTE PTR [rcx],dl
 183:	jne    1f9 <_PyCriticalSection_SuspendAll+0xa9>
 185:	test   r14b,0x2
 189:	jne    213 <_PyCriticalSection_SuspendAll+0xc3>
 18f:	or     r14,0x1 // no load
 193:	mov    QWORD PTR [rdi+0xb0],r14
 ...
 1b8:	xor    ebp,ebp
 1ba:	mov    r15,r14
 1bd:	and    r15,0xfffffffffffffffc
 1c1:	mov    rdi,QWORD PTR [r15+0x8]
 1c5:	test   rdi,rdi
 1c8:	je     1d9 <_PyCriticalSection_SuspendAll+0x89>
 1ca:	mov    al,0x1
 1cc:	lock cmpxchg BYTE PTR [rdi],bpl
 1d1:	jne    20c <_PyCriticalSection_SuspendAll+0xbc>
 1d3:	test   r14b,0x2
 1d7:	jne    241 <_PyCriticalSection_SuspendAll+0xf1>
 1d9:	or     r14,0x1
 1dd:	mov    QWORD PTR [rbx],r14
...

This is not expected to be a hot path, but the change removes redundant loads and memory read-modify-write operations from each iteration. I would appreciate feedback on whether this trade-off is worthwhile.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions