Comment

Comment by quietbritishjim

Yes, numpy does release the GIL. But the code in question has multiple numpy calls, called in a loop:

   sum((np.sin(np.cos(np.sin(np.cos(x + i)))).sum()
        for i in range(n_loop)))
This is not like:

   release GIL
      # i = 0
      compute y = x + 0     (numpy broadcasting sum)
      compute z = np.cos(y) (elementwise)
      ...
      add to running total
      # i = 1
      compute y = x + 1
      ...
   reacquire GIL
Instead it is:

   # i = 0
   look up "+" operation
   release GIL
      compute y = x + 0
   reacquire GIL
   look up "np.cos" operation
   release GIL
      compute z = np.cos(y)
   reacquire GIL
   ...
   # i = 1
   look up "+" operation
   release GIL
      compute y = x + 1
   reacquire GIL
   ...

So there was work being protected by the GIL, that suddenly is exposed to lock contention with free threading.

Of course, without free threading, the lock contention would be way worse, but this time the GIL is the lock being contended. Numpy has to reacquire the GIL whenever it returns from a function call, and this expression is made up of multiple calls. To multithread effectively with numpy (in non-freethreading) you'd normally aim to vectorise into a small number of calls in big arrays.

The composition of +, then np.cos, etc. is not too bad if these are big arrays, but the problem is the pure Python iteration over the range which is, presumably, quite large. You could vectorise over the range:

   x[..., None] + np.arange(n_loop, dtype=np.float64)

but this is the start of a new conversation.

Discussion

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton