Reputation: 3804
I am still new to Python, and I have been trying to improve the performance of my Python script, so I tested it with and without global variables. I timed it, and to my surprise, it ran faster with global variables declared rather than passing local vars to functions. What's going on? I thought execution speed was faster with local variables? (I know globals are not safe, I am still curious.)
Upvotes: 32
Views: 31275
Reputation: 1223
in recent python Versions its changed. global variables are faster in recent versions. here is a benchmark for 100 repeats:
python 3.7
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.046136 │ 0.053999 │ 0.048133 │ 0.001487 │
│ closurefoo │ 0.046756 │ 0.051377 │ 0.048000 │ 0.000754 │
│ defaultfoo │ 0.040886 │ 0.044640 │ 0.042039 │ 0.000771 │
│ localfoo │ 0.044544 │ 0.048356 │ 0.045437 │ 0.000678 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
python 3.11
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.020114 │ 0.021159 │ 0.020465 │ 0.000192 │
│ closurefoo │ 0.023397 │ 0.034848 │ 0.024139 │ 0.001455 │
│ defaultfoo │ 0.021065 │ 0.023610 │ 0.021875 │ 0.000402 │
│ localfoo │ 0.024235 │ 0.026417 │ 0.024608 │ 0.000320 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
python 3.12.1
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.018712 │ 0.020782 │ 0.019288 │ 0.000275 │
│ closurefoo │ 0.021224 │ 0.026080 │ 0.021832 │ 0.000538 │
│ defaultfoo │ 0.018965 │ 0.030679 │ 0.019956 │ 0.001348 │
│ localfoo │ 0.022296 │ 0.023843 │ 0.022958 │ 0.000240 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
but access them inside loop (a loop range 10), is a different story
python 3.7
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.589022 │ 0.602583 │ 0.591952 │ 0.004004 │
│ closurefoo │ 0.548262 │ 0.552037 │ 0.550492 │ 0.001255 │
│ defaultfoo │ 0.534560 │ 0.549248 │ 0.538237 │ 0.004380 │
│ localfoo │ 0.537418 │ 0.540444 │ 0.538876 │ 0.001094 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
python 3.11
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.398205 │ 0.407518 │ 0.401189 │ 0.003344 │
│ closurefoo │ 0.394671 │ 0.399073 │ 0.397187 │ 0.001303 │
│ defaultfoo │ 0.397672 │ 0.399821 │ 0.398848 │ 0.000809 │
│ localfoo │ 0.398310 │ 0.403210 │ 0.400305 │ 0.001574 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
python 3.12.1
┏━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓
┃ Function ┃ Min ┃ Max ┃ Mean ┃ Std ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩
│ globalfoo │ 0.375667 │ 0.381020 │ 0.378422 │ 0.001864 │
│ closurefoo │ 0.352350 │ 0.360654 │ 0.354530 │ 0.002323 │
│ defaultfoo │ 0.352546 │ 0.355952 │ 0.354029 │ 0.001119 │
│ localfoo │ 0.353293 │ 0.357038 │ 0.355307 │ 0.001204 │
└────────────┴──────────┴──────────┴──────────┴──────────┘
Upvotes: 2
Reputation: 394975
When Python compiles a function, the function knows before it is called if the variables in it are locals, closures, or globals.
We have several ways of referencing variables in functions:
So let's create these kinds of variables in a few different functions so we can see for ourselves:
global_foo = 'foo'
def globalfoo():
return global_foo
def makeclosurefoo():
boundfoo = 'foo'
def innerfoo():
return boundfoo
return innerfoo
closurefoo = makeclosurefoo()
def defaultfoo(foo='foo'):
return foo
def localfoo():
foo = 'foo'
return foo
We can see that each function knows where to look up the variable - it doesn't need to do so at runtime:
>>> import dis
>>> dis.dis(globalfoo)
2 0 LOAD_GLOBAL 0 (global_foo)
2 RETURN_VALUE
>>> dis.dis(closurefoo)
4 0 LOAD_DEREF 0 (boundfoo)
2 RETURN_VALUE
>>> dis.dis(defaultfoo)
2 0 LOAD_FAST 0 (foo)
2 RETURN_VALUE
>>> dis.dis(localfoo)
2 0 LOAD_CONST 1 ('foo')
2 STORE_FAST 0 (foo)
3 4 LOAD_FAST 0 (foo)
6 RETURN_VALUE
We can see that currently the byte-code for a global is LOAD_GLOBAL
, a closure variable is LOAD_DEREF
, and a local is LOAD_FAST
. These are implementation details of CPython, and may change from version to version - but it is useful to be able to see that Python treats each variable lookup differently.
Paste into an interpreter and see for yourself:
import dis
dis.dis(globalfoo)
dis.dis(closurefoo)
dis.dis(defaultfoo)
dis.dis(localfoo)
Test code (feel free to test on your system):
import sys
sys.version
import timeit
min(timeit.repeat(globalfoo))
min(timeit.repeat(closurefoo))
min(timeit.repeat(defaultfoo))
min(timeit.repeat(localfoo))
On Windows, at least in this build, it looks like closures get a little bit of a penalty - and using a local that's a default is the fastest, because you don't have to assign the local each time:
>>> import sys
>>> sys.version
'3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.0728403456180331
>>> min(timeit.repeat(closurefoo))
0.07465484920749077
>>> min(timeit.repeat(defaultfoo))
0.06542038103088998
>>> min(timeit.repeat(localfoo))
0.06801849537714588
On Linux:
>>> import sys
>>> sys.version
'3.6.4 |Anaconda custom (64-bit)| (default, Mar 13 2018, 01:15:57) \n[GCC 7.2.0]'
>>> import timeit
>>> min(timeit.repeat(globalfoo))
0.08560040907468647
>>> min(timeit.repeat(closurefoo))
0.08592104795388877
>>> min(timeit.repeat(defaultfoo))
0.06587386003229767
>>> min(timeit.repeat(localfoo))
0.06887826602905989
I'll add other systems as I have a chance to test them.
Upvotes: 17
Reputation: 221
Simple Answer:
Due to Python's dynamic nature, when the interpreter comes across an expression like a.b.c, it looks up a (trying first the local namespace, then the global namespace, and finally the built-in namespace), then it looks in that object's namespace to resolve the name b, and finally it looks in that object's namespace to resolve the name c. These lookups are reasonably fast; For local variables, lookups are extremely fast, since the interpreter knows which variables are local and can assign them a known position in memory.
Interpreter knows which names inside your functions are local and it assigns them specific (known) locations inside the function call's memory. This makes references to locals much faster than to globals and (most especially) to built-ins.
Code example to explain the same:
>>> glen = len # provides a global reference to a built-in
>>>
>>> def flocal():
... name = len
... for i in range(25):
... x = name
...
>>> def fglobal():
... for i in range(25):
... x = glen
...
>>> def fbuiltin():
... for i in range(25):
... x = len
...
>>> timeit("flocal()", "from __main__ import flocal")
1.743438959121704
>>> timeit("fglobal()", "from __main__ import fglobal")
2.192162036895752
>>> timeit("fbuiltin()", "from __main__ import fbuiltin")
2.259413003921509
>>>
Upvotes: 18
Reputation: 54242
According to this page on locals and globals:
When a line of code asks for the value of a variable x, Python will search for that variable in all the available namespaces, in order:
- local namespace - specific to the current function or class method. If the function defines a local variable x, or has an argument x, Python will use this and stop searching.
- global namespace - specific to the current module. If the module has defined a variable, function, or class called x, Python will use that and stop searching.
- built-in namespace - global to all modules. As a last resort, Python will assume that x is the name of built-in function or variable.
Based on that, I'd assume that local variables are generally faster. My guess is what you're seeing is something particular about your script.
Here's a trivial example using a local variable, which takes about 0.5 seconds on my machine (0.3 in Python 3):
def func():
for i in range(10000000):
x = 5
func()
And the global version, which takes about 0.7 (0.5 in Python 3):
def func():
global x
for i in range(1000000):
x = 5
func()
global
does something weird to variables that are already globalInterestingly, this version runs in 0.8 seconds:
global x
x = 5
for i in range(10000000):
x = 5
While this runs in 0.9:
x = 5
for i in range(10000000):
x = 5
You'll notice that in both cases, x
is a global variable (since there's no functions), and they're both slower than using locals. I have no clue why declaring global x
helped in this case.
This weirdness doesn't occur in Python 3 (both versions take about 0.6 seconds).
If you want to optimize your program, the best thing you can do is profile it. This will tell you what's taking the most time, so you can focus on that. Your process should be something like:
Upvotes: 42
Reputation: 13189
The time that you are not including is the programmer time spent tracking down the bugs created when using a global has a side effect somewhere else in your program. That time is many times greater than the time spent creating and freeing local variables,
Upvotes: 10