I suppose you might be using f = lambdify(x, sin(x), 'scipy'). This produces a function that calls scipy.sin instead of numpy.sin. However, scipy.sin is just a deprecated wrapper for np.sin, which would explain why it is a little slower.
I'm a little unclear why it doesn't raise a deprecation warning when you call it in the lambdified function, but you can confirm that it is indeed using scipy.sin by looking at f.__globals__['sin']. In any case, you should generally not just use 'scipy' by itself, but rather ['numpy', 'scipy']. Actually lambdify already includes scipy by default, so there's no need to specify either. See https://github.com/sympy/sympy/issues/20294. Aaron Meurer On Thu, Sep 16, 2021 at 3:58 PM Aaron Meurer <asmeu...@gmail.com> wrote: > What is the difference between "numpy" and "scipy" in your graph? I > wouldn't expect scipy to make any difference for sin or cos which are NumPy > functions. > > Aaron Meurer > > On Wed, Sep 15, 2021 at 7:19 AM Davide Sandona' <sandona.dav...@gmail.com> > wrote: > >> Thanks Oscar for the wonderful clarification! >> >> I rerun my code with SYMPY_USE_CACHE=no, now the results make much more >> sense. >> >> [image: lambdify-no-cache.png] >> >> Davide. >> >> >> Il giorno mer 15 set 2021 alle ore 15:14 Oscar Benjamin < >> oscar.j.benja...@gmail.com> ha scritto: >> >>> >>> On Wed, 15 Sept 2021 at 13:41, Oscar Benjamin < >>> oscar.j.benja...@gmail.com> wrote: >>> >>>> On Tue, 14 Sept 2021 at 23:12, sandona...@gmail.com < >>>> sandona.dav...@gmail.com> wrote: >>>> >>>>> Hello, >>>>> let's say I'd like to numerically evaluate a single sympy function >>>>> over an array using sympy as the module. Curiously, passing in regular >>>>> Python's float numbers makes the evaluation much faster then passing in >>>>> Sympy's Float instances. I tried several sympy functions, they tend to >>>>> follow this trend. >>>>> >>>> >>>> The 3 millisecond timing difference that you are asking about here is >>>> dwarfed by the actual 1 second time that it really takes to compute this >>>> result the first time. Most likely the time differences you see are just to >>>> do with exactly how efficient the cache lookups are for different types. >>>> >>> >>> If you want to see what is taking the time then use a profiler. From >>> isympy/ipython you can use %prun or if you want to run separate processes >>> you can use python -m cProfile but that will also time how long it takes to >>> import sympy so it's only useful for things that take at least several >>> seconds. >>> >>> In the first run the time is taken by sin.eval and most of that by the >>> extract_multiplicatively function. >>> >>> In [2]: %prun -s cumulative f(domain) >>> >>> 2069604 function calls (2047615 primitive calls) in 1.585 >>> seconds >>> >>> Ordered by: cumulative time >>> >>> ncalls tottime percall cumtime percall filename:lineno(function) >>> 1 0.000 0.000 1.585 1.585 {built-in method >>> builtins.exec} >>> 1 0.001 0.001 1.585 1.585 <string>:1(<module>) >>> 1000 0.002 0.000 1.584 0.002 >>> <lambdifygenerated-1>:1(_lambdifygenerated) >>> 16989/1000 0.022 0.000 1.582 0.002 cache.py:69(wrapper) >>> 1000 0.014 0.000 1.579 0.002 function.py:450(__new__) >>> 1000 0.013 0.000 1.392 0.001 function.py:270(__new__) >>> 1000 0.012 0.000 1.270 0.001 trigonometric.py:266(eval) >>> 2997 0.029 0.000 0.988 0.000 >>> expr.py:2183(extract_multiplicatively) >>> 7993 0.020 0.000 0.835 0.000 assumptions.py:460(getit) >>> 8993 0.336 0.000 0.722 0.000 >>> facts.py:499(deduce_all_facts) >>> 6993/2997 0.007 0.000 0.529 0.000 >>> decorators.py:88(__sympifyit_wrapper) >>> 2997 0.010 0.000 0.525 0.000 >>> numbers.py:1315(__truediv__) >>> 999 0.005 0.000 0.511 0.001 >>> expr.py:2453(could_extract_minus_sign) >>> 999 0.001 0.000 0.505 0.001 >>> expr.py:1662(as_coefficient) >>> 999 0.001 0.000 0.480 0.000 numbers.py:759(__truediv__) >>> 999 0.001 0.000 0.478 0.000 decorators.py:254(_func) >>> 999 0.001 0.000 0.476 0.000 >>> decorators.py:129(binary_op_wrapper) >>> 999 0.003 0.000 0.474 0.000 expr.py:260(__truediv__) >>> 4996 0.015 0.000 0.459 0.000 assumptions.py:472(_ask) >>> 999 0.015 0.000 0.457 0.000 operations.py:46(__new__) >>> 999 0.037 0.000 0.408 0.000 mul.py:178(flatten) >>> 3997 0.005 0.000 0.357 0.000 assumptions.py:444(copy) >>> 3997 0.014 0.000 0.352 0.000 >>> assumptions.py:432(__init__) >>> 198823 0.074 0.000 0.254 0.000 {built-in method >>> builtins.all} >>> 570492 0.158 0.000 0.209 0.000 facts.py:533(<genexpr>) >>> 1999 0.032 0.000 0.126 0.000 sets.py:1774(__new__) >>> 183867 0.083 0.000 0.083 0.000 facts.py:482(_tell) >>> 999 0.007 0.000 0.078 0.000 evalf.py:1425(evalf) >>> 15987 0.023 0.000 0.077 0.000 sympify.py:92(sympify) >>> 1000 0.003 0.000 0.066 0.000 function.py:214(nargs) >>> 7996 0.026 0.000 0.065 0.000 >>> compatibility.py:501(ordered) >>> 6994 0.015 0.000 0.063 0.000 numbers.py:1197(_new) >>> 29977 0.020 0.000 0.057 0.000 <frozen >>> importlib._bootstrap>:1009(_handle_fromlist) >>> 391655 0.054 0.000 0.054 0.000 {method 'get' of 'dict' >>> objects} >>> 1998/999 0.007 0.000 0.053 0.000 evalf.py:1332(evalf) >>> 1998 0.053 0.000 0.053 0.000 mul.py:449(_gather) >>> 999 0.005 0.000 0.046 0.000 evalf.py:781(evalf_trig) >>> ... >>> >>> Now a second run in the same process just shows 1000 cache lookups: >>> >>> In [3]: %prun -s cumulative f(domain) >>> >>> 2003 function calls in 0.002 seconds >>> >>> Ordered by: cumulative time >>> >>> ncalls tottime percall cumtime percall filename:lineno(function) >>> 1 0.000 0.000 0.002 0.002 {built-in method >>> builtins.exec} >>> 1 0.001 0.001 0.002 0.002 <string>:1(<module>) >>> 1000 0.001 0.000 0.001 0.000 >>> <lambdifygenerated-1>:1(_lambdifygenerated) >>> 1000 0.001 0.000 0.001 0.000 cache.py:69(wrapper) >>> 1 0.000 0.000 0.000 0.000 {method 'disable' of >>> '_lsprof.Profiler' objects} >>> >>> This is the second run when the inputs are Expr: >>> >>> In [5]: %prun -s cumulative f(domain_sympy) >>> 5003 function calls in 0.014 seconds >>> >>> Ordered by: cumulative time >>> >>> ncalls tottime percall cumtime percall filename:lineno(function) >>> 1 0.000 0.000 0.014 0.014 {built-in method >>> builtins.exec} >>> 1 0.007 0.007 0.014 0.014 <string>:1(<module>) >>> 1000 0.002 0.000 0.008 0.000 >>> <lambdifygenerated-1>:1(_lambdifygenerated) >>> 1000 0.002 0.000 0.006 0.000 cache.py:69(wrapper) >>> 1000 0.001 0.000 0.004 0.000 numbers.py:1480(__hash__) >>> 1000 0.001 0.000 0.002 0.000 numbers.py:806(__hash__) >>> 1000 0.001 0.000 0.001 0.000 expr.py:126(__hash__) >>> 1 0.000 0.000 0.000 0.000 {method 'disable' of >>> '_lsprof.Profiler' objects} >>> >>> The cache lookups here are slower because the pure Python Expr.__hash__ >>> methods are slower than for numpy's float64.__hash__ which is implemented >>> in C. >>> >>> Either way this shows that the differences are all just to do with >>> caching and are not representative of the actual time that it would take to >>> compute these results once. The bulk of the time in the first run is >>> ultimately consumed by deduce_all_facts i.e. the old assumptions. Note that >>> the old assumptions are themselves cached on each Basic instance separately >>> from the main cache which can also affect timing results if you don't use a >>> separate process for timings: >>> >>> In [1]: e = sin(1) >>> >>> In [2]: e._assumptions >>> Out[2]: {} >>> >>> In [3]: e.is_real >>> Out[3]: True >>> >>> In [4]: e._assumptions >>> Out[4]: >>> {'real': True, >>> 'positive': True, >>> 'finite': True, >>> 'infinite': False, >>> 'extended_positive': True, >>> 'extended_real': True, >>> 'commutative': True, >>> 'imaginary': False, >>> 'hermitian': True, >>> 'complex': True, >>> 'extended_nonnegative': True, >>> 'nonpositive': False, >>> 'negative': False, >>> 'extended_nonpositive': False, >>> 'extended_nonzero': True, >>> 'extended_negative': False, >>> 'zero': False, >>> 'nonnegative': True, >>> 'nonzero': True} >>> >>> The deduce_all_facts function is the one that applies all of the >>> implications relating these different assumption predicates so that as soon >>> as one predicate (e.g. positive=True) is known then all of the others can >>> be stored in the _assumptions dict. Although this makes things faster on a >>> second assumptions query it does in fact consume the bulk of the time for >>> computing many things in SymPy. >>> >>> Assumptions queries that take place during automatic evaluation are >>> often a cause of slowness in SymPy. The bottom of the profile report shows >>> evalf being called 2000 times i.e. twice per evaluation of the sin >>> function. The evalf calls are part of the assumptions query and are >>> reasonably fast for a simple sin(float) but can be very expensive for large >>> expressions. >>> >>> >>> -- >>> Oscar >>> >>> -- >>> You received this message because you are subscribed to the Google >>> Groups "sympy" group. >>> To unsubscribe from this group and stop receiving emails from it, send >>> an email to sympy+unsubscr...@googlegroups.com. >>> To view this discussion on the web visit >>> https://groups.google.com/d/msgid/sympy/CAHVvXxRmA9AYL3TnAJeM_crc%2BKc9kb3ADtLR%2BptZEQKYPbKspw%40mail.gmail.com >>> <https://groups.google.com/d/msgid/sympy/CAHVvXxRmA9AYL3TnAJeM_crc%2BKc9kb3ADtLR%2BptZEQKYPbKspw%40mail.gmail.com?utm_medium=email&utm_source=footer> >>> . >>> >> -- >> You received this message because you are subscribed to the Google Groups >> "sympy" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to sympy+unsubscr...@googlegroups.com. >> To view this discussion on the web visit >> https://groups.google.com/d/msgid/sympy/CAO%3D1Z0_LqKn2syviLVpBVqOkZMCWC--5g_Z55yhndhs%3D5ZjX-A%40mail.gmail.com >> <https://groups.google.com/d/msgid/sympy/CAO%3D1Z0_LqKn2syviLVpBVqOkZMCWC--5g_Z55yhndhs%3D5ZjX-A%40mail.gmail.com?utm_medium=email&utm_source=footer> >> . >> > -- You received this message because you are subscribed to the Google Groups "sympy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sympy+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/sympy/CAKgW%3D6L1ND5oJJFb6cKAGYNyuribHipAUWG0ejw0YLvGzvtBkA%40mail.gmail.com.