Optimizing Python: Why Python Is Slow and 4 Optimization Methods
What is Python Optimization?
Python is a popular programming language used for web development, machine learning, and many other applications. Its main advantage is that it is easy to learn, easy to use, and highly versatile, supporting developer productivity.
However, Python is also notoriously slow. Programs written in languages such as C++, Node.js, and Go can execute as much as 30-40 times faster than equivalent Python programs. There are several reasons, including the fact that Python is an interpreted language (code needs to be compiled during runtime), is dynamically typed, and runs on a single thread.
Python optimization is the process of improving the performance of Python programs, despite the inherent disadvantages of the technology. We’ll cover common strategies for Python optimization, including profiling, code mapping, removing redundancy, and the use of application performance monitoring (APM) technology.
In this article:
- Why Python is Slow: Common Python Performance Issues
- How Does Python Performance Compare to Other Languages?
- 5 Python Optimization Methods
Why Python is Slow: Common Python Performance Issues
Python is an Interpreted Language
One of the most important differences between Python and other languages is that Python is an interpreted language—Python programs are able to run without requiring the program to be compiled into machine language.
Interpreted languages tend to perform worse than compiled languages, because each instruction coded by the programmer requires more machine instructions to execute. In compiled languages, the compiler creates executables containing bytecode—binary machine code that can be read directly by the machine’s processor—meaning that it is less resource intensive to execute the required operations.
Python Uses Ahead-of-Time Compilation
In Python specifically, code has to be interpreted every time it runs on the processor. To avoid compiling code every time it runs, Python uses compiled .pyc files, which allow caching of bytecode generated from raw Python code. However, this does not perform as well as equivalent Java or .NET files, because those languages use Just-In-Time Compilation (JIT).
Python typically uses an ahead-of-time (AOT) compiler, which needs to make sure the CPU understands every piece of code before execution. This means the compiler misses many opportunities for optimization. JIT, on the other hand, can perform many optimizations during compilation. Some JIT compilers are even smart enough to recognize a block of code that is referenced multiple times and replace it with a more efficient version. So, while they execute the same sequence of bytecodes, they can do so much faster than an AOT compiler.
Python is catching up with languages like Java and .NET—some Python implementations like PyPy already include a JIT compiler.
Python is Dynamically Typed
One of the great things about Python is that you don’t have to define a type every time you declare a variable. Take this example:
a = 0
b = a
a = “some text”
Note that neither of the variables have a type defined. Here is what happens when Python executes this code:
- Python creates an integer object in memory, assigns it the value of 0, then creates a variable called a which references that object.
- Python creates another integer object, gives it the same value as a, and creates a new variable b that references the second integer object.
- In the third line, the variable a is surprisingly set to a string. Python creates a new string object, and variable a now points to this new object. The value of b remains unchanged—it still points to the same integer object that was created before.
This behavior is very convenient for developers and is one of the major reasons for Python’s popularity. However, this has a large impact on language performance. In a dynamically typed language, the interpreter is unaware of the types of variables defined at program runtime. This means that additional effort is required to identify the type of data stored in a Python variable so that it can be used in a statement.
Whenever developers leverage Python’s dynamic typing capabilities, like in the code above, equivalent C code will outperform the Python code. This is because:
- When the data type of a variable is explicitly declared, it is easy to improve performance by applying optimizations appropriate to that data type.
- The runtime environment does not need to evaluate the type of data stored in the variable every time it is accessed, reducing duplicate work.
To summarize, dynamic typing is the main reason why Python is slow but also hugely popular in the tech community.
Single-thread vs. Multi-threaded
In some languages, such as Java, code can run in parallel on multiple CPUs. However, Python is by design single-threaded, running code on a single CPU. The mechanism for ensuring this is called Global Interpreter Lock (GIL)—it ensures that the interpreter only executes one thread at any given time.
The motivation behind GIL is to allow Python to use reference counting for memory management. A variable’s reference count must be protected in situations where two threads increment or decrement the count at the same time. This can lead to bugs and memory leaks—for example, when an object is no longer needed but is never deleted, or when a variable is deleted, but other variables still depend on it.
Python’s single-threaded design protects against memory leaks in many situations, making it more reliable and stable, and eliminating complex memory management mechanisms used by languages like Java. However, the need to run on a single thread limits the scalability and performance of Python programs.
Related content: Read our guide to Python performance (coming soon)
How Does Python Performance Compare to Other Languages?
Here is how Python measures up to other popular programming languages:
- Python vs. C++—C++ is a compiled language, and so naturally offers better performance than Python. However, it is more difficult to learn and has fewer supporting libraries than Python, slowing down development work.
- Python vs. Java—Java is widely used for enterprise applications, is the primary language for Android mobile applications, and is also commonly used for internet of things (IoT) devices. Java uses an efficient JIT compiler which gives it a natural performance advantage over Python. In addition, Java provides advanced debugging tools that help discover runtime errors more quickly.
- Python vs. C#—C# is an object-oriented language created by Microsoft, which combines the processing efficiency of C++ with ease of use. However, like the languages above, C# is a compiled language. Research has shown that C# code can run more than 40 times faster than Python. Even if you speed up Python with PyPy, which has its own JIT compiler, C# performs considerably better.
- Python vs. PHP—PHP is a popular server-side programming language for web applications, which can be easily embedded into HTML. PHP, like Python, is an interpreted language, and is slow relative to the languages discussed above. The recent release of Python 7, which includes Zend Engine 3.0, improved code interpreting speed by as much as 2X. At this time, Python 7 beats most PHP implementations in terms of raw speed, memory usage, CPU load, and code size.
- Python vs. Go—Go is a language built to simplify the software development process. It was designed to provide better ease of use than languages like C++, while still offering high performance. Research shows that Go runs up to 30 times faster than Python, mainly due to Go’s support for concurrency and parallelism. This also makes Go highly scalable—not a surprise given that it was developed by Google. Go easily beats Python due to its multi-threaded architecture.
5 Python Optimization Methods
1. Python Profiling
Profiling is a way to programmatically analyze software bottlenecks. It involves analyzing memory usage, number of function calls, and the execution time of those calls. This analysis is important because it provides a way to detect slow or resource-inefficient parts of a software program, enabling optimization of the program.
Python provides useful tools for analyzing software in terms of runtime and memory. One of the most widely used mechanisms is the timeit method, which provides an easy way to measure the execution time of a software program. Python also provides the memory_profile module, which lets you measure memory usage of lines of code in Python scripts. Both methods can be easily implemented with a few lines of code.
Continuous profilers, like gProfiler, have an always-on approach. They sample the CPU, allowing you to investigate performance at any given time to see the impact of version releases, understand the performance impact of new features, compare deployments and isolate performance issues. By continuously analyzing code performance across your entire environment, you can optimize the most resource-consuming parts of your code, improve application performance, and reduce costs.
2. Optimizing Loops Using Maps
Loop optimization is important for Python performance. Loops are common in coding, and Python has built-in processes that support loops. However, these processes usually slow down a Python program. Code mapping makes better use of time and speeds up the execution of loops.
Code maps are built-in structures for simplifying complex code. They make code easier to share and define without using too many parameters or functions. This allows you to efficiently convert dozens of lines of code into one.
3. Removing Dead Code
Dead code consumes processing power and slows Python optimizations and code output. Review your Python code regularly and remove unnecessary or redundant code to save memory. Here are common strategies for removing unused code:
- Context managers—these are mechanisms that hide or unhide function code. When a function is “hidden”, it does not block memory. This can speed up your programs and clean up your code by freeing memory for the code that actually needs to run.
- Multiprocessing—while Python does not support multi-threaded operation, you can run multiple Python processes concurrently, using additional memory banks separate from current working memory. You can access memory on additional servers to efficiently run background processes.
- Preload processes—if a program performs operations that takes a long time to complete, you can preload these operations, and have the output ready when required for common tasks.
4. Use Application Performance Monitoring Tools
A common tip for Python performance is to benchmark a program, identify performance bottlenecks, and try to release them with optimization. However, in real Python projects this can be very complex. In a large Python program it can be nearly impossible to isolate a suspicious piece of code.
Application performance monitoring (APM) can help by monitoring the execution of a Python program, analyzing the performance impact of different operations, and tying them back to inefficient code or elements in the runtime environment. APM can take into account how the entire application performs in a production environment. This provides insights that make it much easier to optimize a Python program’s performance.
5. Use Continuous Profiling Tools Like gProfiler
Continuous profilers, like gProfiler, have an always-on approach. They sample the CPU, allowing you to investigate performance at any given time to see the impact of version releases, understand the performance impact of new features, compare deployments and isolate performance issues.
By continuously analyzing code performance across your entire environment, you can optimize the most resource-consuming parts of your code, improve application performance, and reduce costs.
gProfiler is open source and free, allowing all developers to commoditize performance and expand the repository over time. The solution allows DevOps teams to profile any timeframe at any granularity to get the most accurate view of their environment, pinpoint optimization opportunities, and improve application performance.