Estimate Asymptotic Runtime Complexity from Bytecode executions
Project description
The Python Performance Analysis Library (py-pal) is a profiling tool for the Python programming language. With py-pal one can approximate the time complexity (big O notation) of Python functions in an empirical way. The arguments of the function and the executed opcodes serve as a basis for the analysis.
To the docs.
Installation
Requirements
- An installation of the CPython implementation of the Python programming language of version greater or equal to 3.7
- A compiler for the C/C++ programming language:
- On Microsoft Windows, we use the Buildtools für Visual Studio 2019:
https://visualstudio.microsoft.com/de/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16
On Linux, any C compiler supported by Cython e.g. g++
Install py-pal via pip by running:
This project requires CPython and a C compiler to run. Install CPython >= 3.7, then install py-pal by running:
pip install py-pal
or
python -m pip install py-pal
Command line usage of the py-pal module
python -m py_pal <target-module/file>
or
py-pal <target-module/file>
There are multiple aliases to the same command: py-pal, py_pal and pypal. If py-pal is executed this way, all functions called in the code are captured and analyzed. The output is in the form of a pandas data frame.
See the help message:
py-pal -h
py-pal can perform cost analysis on a line-by-line basis:
py-pal <file> -l/–line
The –separate flag can be used to examine the cost function of individual arguments (caution: this function assumes the independence of the arguments):
py-pal <file> -s/–separate
The output of the results can be noisy, to limit this you can use –filter-function to filter the desired functions from the result. Regular expressions are supported:
py-pal <file> -ff/–filter-function .*_sort
Similarly, the result can also be filtered by modules with –filter-module, e.g. to exclude importlib modules
py-pal <file> -fm/–filter-module “^(?!<frozen.*>).*”
To save the results in a specified folder use –out:
py-pal <file> -o/–out results
The output format can be changed with –format:
py-pal <file> -o/–out results –format json
With the additional specification of the –plot flag, the cost functions of the result set are stored as images:
py-pal <file> -o/–out results -p/–plot
For the –log-level flag see the development docs.
Example, creating plots for selected functions:
py-pal tests/examples/sort.py -o results -p -f sort
Programmatic usage of the py-pal module
To profile a single function and get the complexity estimate there is profile_function.
from py_pal.core import profile_function
from py_pal.data_collection.opcode_metric import OpcodeMetric
from py_pal.datagen import gen_random_growing_lists
from algorithms.sort import bubble_sort
profile_function(OpcodeMetric(), gen_random_growing_lists(), bubble_sort)
The profile decorator:
from py_pal.core import profile, DecoratorStore
@profile
def test():
pass
# Must be called at some point
test()
estimator = AllArgumentEstimator(DecoratorStore.get_call_stats(), DecoratorStore.get_opcode_stats())
res = estimator.export()
By using the profile decorator, it is possible to annotate Python functions such that only the annotated Python functions will be profiled. It acts similar to a whitelist filter.
Another possibility is to use the context-manager protocol:
from py_pal.analysis.estimator import AllArgumentEstimator
from py_pal.data_collection.tracer import Tracer
with Tracer() as t:
pass
estimator = AllArgumentEstimator(t.get_call_stats(), t.get_opcode_stats())
res = estimator.export()
# Do something with the resulting DataFrame
print(res)
The most verbose way to use the py-pal API:
from py_pal.analysis.estimator import AllArgumentEstimator
from py_pal.data_collection.tracer import Tracer
t = Tracer()
t.trace()
# Your function
pass
t.stop()
estimator = AllArgumentEstimator(t.get_call_stats(), t.get_opcode_stats())
res = estimator.export()
# Do something with the resulting DataFrame
print(res)
All examples instantiate a tracer object that is responsible for collecting the data. After execution, the collected data is passed to the analysis module. Finally, an estimate of the asymptotic runtime of the functions contained in the code is obtained.
Modes
In the current version py-pal offers only the profiling mode. Although py_pal.datagen offers some functions for generating inputs, py-pal must be combined with appropriate test cases to realize a performance testing mode. An automatic detection and generation of appropriate test inputs does not exist at the moment.
Limitations
The profiling approach implemented by the py-pal modules does not distinguish between different threads executing a Python function. Actually it is a major problem to profile a Python script which makes use of threads. The bytecode counting strategy will increase all counters of Python functions on the current call stack no matter what threads is executing it. Thus, the data points will not be accurate to what really happened during the profiled execution of the script.
Licensing Notes
This work integrates some code from the big_O project. More specifically, most code in py_pal.analysis.complexity, py_pal.datagen and py_pal.analysis.estimator.Estimator.infer_complexity is adapted from bigO.
Changelog
What’s New in Py-PAL 1.3.0
Command line interface changes:
Renamed -f/–function to -ff/–filter-function
Added -fm/–filter-module functionality to filter results by module
Py-PAL 1.2.0
Improved the statistics and plotting output
Command line interface changes:
Deprecated –save flag in favor of -o/–out
Renamed -V/–visualize to -p/–plot
Change functionality of -f/–function from executing and profiling a specific function inside a python file to applying the analysis to a selected function. Regular expressions are suported.
Py-PAL 1.1.0
Improved Data Collection: The heuristic for determining the size of function arguments has been improved.
More tests
More documentation
More argument generation functions in py_pal.datagen
Replaced command line option –debug with –log-level for more configurable log output
Refactoring
Project structure changes, overall CLI interface is unchanged. API changes:
py_pal.tracer moved to py_pal.data_collection.tracer
py_pal.complexity and py_pal.estimator moved to the py_pal.analysis package.
py_pal.analysis.estimator.Estimator now takes call and opcode stats as arguments.
Py-PAL 1.0.0
More thorough testing from different combinations of requirements and Python versions.
Bug fixes
Py-PAL 0.2.1
Refactoring
The estimator module was refactored which introduces a slight change to the API. Classes inheriting from Estimator now only specify how to transform the collected data with respect to the arguments of the function.
Instead of ComplexityEstimator you should use the AllArgumentEstimator class. Additionally there is the SeparateArgumentEstimator which is experimental.
Py-PAL 0.1.6
More accurate Data Collection
The Tracer is enhanced by measuring builtin function calls with AdvancedOpcodeMetric.
Opcodes resembling a function call .e.g FUNCTION_CALL are filtered for built in function calls. If the called function is found in the complexity mapping a synthetic Opcode weight gets assigned. A builtin function call is evaluated using its argument and a pre-defined runtime complexity e.g. O(n log n) for sort().
The feature is enabled by default
The calculation produces a performance overhead and can be disabled by providing a OpcodeMetric instance to the Tracer
The AdvancedOpcodeMetric instance assigned to the Tracer provides statistics about how many builtin function calls were observed and how many were found in the complexity map
Bugfixes
Cleaning data after normalization introduced wrong data points
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Hashes for py_pal-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 62a94cdfe22906fdce11fecf5b73b7d245278c64e185645aad7eb3414a182503 |
|
MD5 | f5f7c534c13058cb45e8dbb441262213 |
|
BLAKE2b-256 | c5fb3a3b01cd74a1d43e1bed0b811ab7eb9fc70fed6011dc1a9d91c329706512 |
Hashes for py_pal-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2599cf1bdcd71955a32d852564a51faf6848dbbe97078378f2c971a44b2cb36a |
|
MD5 | 003aadc4a6b956e39e20a0199b78df4c |
|
BLAKE2b-256 | 73dab600361c5f54bd3f9e449c8f2405e13f89826649d3d4856a7fc1c6b47fbd |
Hashes for py_pal-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ae0d79f50cbd7d915c76a1631f824c0d93392a12d72be96006c067aaefc209d6 |
|
MD5 | 3890ffded6dc01057805120b330e6cf7 |
|
BLAKE2b-256 | 225842332336d618cd0784016c4e8271a749bb5a0548b26b78a3e4389642bd61 |
Hashes for py_pal-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 60daf7d0a06ec211f16478a40dd75f809e9316bc41ead911c9d979e7641403ab |
|
MD5 | 8d5481b6182dbd6a511b87e286314e66 |
|
BLAKE2b-256 | 731ef4e94a9f94b884f6a14ea89390c60c3735672922d50feb73fa6b0769b830 |
Hashes for py_pal-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 93aa5220dfca66e9cff0ad8570a5cf3354fadb46a791e06c3d7023ba7fab430f |
|
MD5 | ce5a826c55dfda8b598fdbe6e329d061 |
|
BLAKE2b-256 | d9dc7e40293405f1cf22f4d4a462a477331faabc41feed2ccf894d5530ed9519 |
Hashes for py_pal-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | a22d0e6ef0de734e270b3c89787b442b49ee46cc63239a54c25c947b9e85e2be |
|
MD5 | ae3e3d9a212873283267d0a95201e874 |
|
BLAKE2b-256 | 9a3981c247f60b9c3b58daf0f098a996fa70c89c8a3a85b75125e4531241da74 |
Hashes for py_pal-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5abd9dcddee5cc0f7474aa2f0d835ea229a45a5afacda6ed61815a9de535335b |
|
MD5 | ecd6d87809c71035b07d572a00cba0f9 |
|
BLAKE2b-256 | 535595bb2c16d2ae035683b88ae0f7581c10a9c57aa156fab173a4ecd95a9268 |
Hashes for py_pal-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8ccb2bb3f25af179696c7c3d9afbe1a37a16a6f6c744ba7968189c3f3fe43250 |
|
MD5 | 1f03e1404b7e51b9275f0c741f6b6f95 |
|
BLAKE2b-256 | a0ce64a4cae9a0330d317f31b71b3bb5c408934f442de97d7832201ddcba33c6 |
Hashes for py_pal-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 146dbde17d55823de5f7e840c2ee072161c676785c8025603cf629cf64b36ee6 |
|
MD5 | f50e280295ab01849887eea311806d53 |
|
BLAKE2b-256 | 8d1f8da9089af40bcd46cc30a810385d70b19367f8adba25b18da365a4da504c |