title: Running time scales with the number of threads when running a function received from Python inside OpenMP parallel block
body: <p>Here are the files for test.</p>
  <pre><code># CMakeLists.txt
  
  cmake_minimum_required(VERSION 3.16)
  project(CALLBACK_TEST)
  
  set(CMAKE_CXX_STANDARD 17)
  add_compile_options(-O3 -fopenmp -fPIC)
  add_link_options(-fopenmp)
  
  add_subdirectory(pybind11)
  pybind11_add_module(callback callback.cpp)
  
  add_custom_command(TARGET callback POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E create_symlink $&lt;TARGET_FILE:callback&gt; ${CMAKE_CURRENT_SOURCE_DIR}/callback.so
  )
  </code></pre>
  <pre class="lang-cpp prettyprint-override"><code>// callback.cpp
  
  #include &lt;cmath&gt;
  #include &lt;functional&gt;
  #include &lt;vector&gt;
  
  #include &lt;pybind11/pybind11.h&gt;
  #include &lt;pybind11/functional.h&gt;
  
  namespace py = pybind11;
  
  class C
  {
  public:
    C(std::function&lt;float(float)&gt; f, size_t s) : f_(f), v_(s, 1) {}
    void apply()
    {
  #pragma omp parallel for
      for (size_t i = 0; i &lt; v_.size(); i++)
        v_[i] = f_(v_[i]);
    }
    void apply_direct()
    {
  #pragma omp parallel for
      for (size_t i = 0; i &lt; v_.size(); i++)
        v_[i] = log(1 + v_[i]);
    }
  
  private:
    std::vector&lt;float&gt; v_;
    std::function&lt;float(float)&gt; f_;
  };
  
  PYBIND11_MODULE(callback, m)
  {
    py::class_&lt;C&gt;(m, &quot;C&quot;)
        .def(py::init&lt;std::function&lt;float(float)&gt;, size_t&gt;())
        .def(&quot;apply&quot;, &amp;C::apply, py::call_guard&lt;py::gil_scoped_release&gt;())
        .def(&quot;apply_direct&quot;, &amp;C::apply_direct);
    m.def(&quot;log1p&quot;, [](float x) -&gt; float
          { return log(1 + x); });
  }
  </code></pre>
  <pre class="lang-py prettyprint-override"><code># callback.py
  
  import math
  import time
  
  from callback import C, log1p
  
  
  def run(n, func):
      start = time.time()
      if func:
          for _ in range(n):
              c = C(func, 1000)
              c.apply()
      else:
          for _ in range(n):
              c = C(func, 1000)
              c.apply_direct()
      end = time.time()
      print(end - start)
  
  
  if __name__ == &quot;__main__&quot;:
      n = 1000
      one = 1
      print(&quot;Python&quot;)
      run(n, lambda x: math.log(x + 1))
      print(&quot;C++&quot;)
      run(n, log1p)
      print(&quot;Direct&quot;)
      run(n, None)
  </code></pre>
  <p>I run the Python script on a server with 48 CPU cores. Here is the running time. It shows 1. the running time increases when <code>OMP_NUM_THREADS</code> increases especially when accepting the Python/C++ callback from Python, and 2. keeping everything inside C++ is much faster, which seems to contradict the &quot;no overhead&quot; claim as in the <a href="https://pybind11.readthedocs.io/en/stable/advanced/cast/functional.html" rel="nofollow noreferrer">documentation</a>.</p>
  <pre><code>$ python callback.py
  Python
  19.612852573394775
  C++
  19.268250226974487
  Direct
  0.04382634162902832
  $ OMP_NUM_THREADS=4 python callback.py
  Python
  6.042902708053589
  C++
  5.48648738861084
  Direct
  0.03322458267211914
  $ OMP_NUM_THREADS=1 python callback.py
  Python
  0.5964927673339844
  C++
  0.38849639892578125
  Direct
  0.020793914794921875
  </code></pre>
  <p>And when OpenMP is turned off:</p>
  <pre><code>$ python callback.py
  Python
  0.8492450714111328
  C++
  0.26660943031311035
  Direct
  0.010872125625610352
  </code></pre>
  <p>So what goes wrong here?</p>