Skip to content

⚡️ Speed up function _version_split by 38%#19

Open
codeflash-ai[bot] wants to merge 1 commit intoopt-attempt-2from
codeflash/optimize-_version_split-mjjlqufy
Open

⚡️ Speed up function _version_split by 38%#19
codeflash-ai[bot] wants to merge 1 commit intoopt-attempt-2from
codeflash/optimize-_version_split-mjjlqufy

Conversation

@codeflash-ai
Copy link
Copy Markdown

@codeflash-ai codeflash-ai Bot commented Dec 24, 2025

📄 38% (0.38x) speedup for _version_split in src/packaging/specifiers.py

⏱️ Runtime : 1.93 milliseconds 1.40 milliseconds (best of 6 runs)

📝 Explanation and details

The optimization adds a pre-filter check before invoking the expensive regex operation, avoiding regex overhead for components that cannot possibly match the pattern.

What changed:
The code now checks if item and item[0].isdigit() and not item.isdigit() before calling _prefix_regex.fullmatch(item). This condition identifies items that:

  1. Are non-empty
  2. Start with a digit
  3. Are NOT purely numeric (meaning they contain mixed alphanumeric content)

Only these candidates can match the pattern ([0-9]+)((?:a|b|c|rc)[0-9]+) (e.g., "1a2", "3rc5"). The optimization skips regex matching for purely numeric items (like "1", "2", "3") and non-numeric items (like "foo", ""), which comprise the majority of version components in typical usage.

Why it's faster:
Regex operations in Python have significant overhead - even for non-matches, the engine must parse and evaluate the pattern. The line profiler shows:

  • Original: _prefix_regex.fullmatch(item) was called 10,411 times, consuming 5.2 million nanoseconds (25.8% of total time)
  • Optimized: The regex is now called only 2,638 times (a 75% reduction), consuming just 1.2 million nanoseconds (8% of total time)

The fast-path checks (item[0].isdigit() and not item.isdigit()) are native string operations implemented in C, making them orders of magnitude faster than regex compilation and matching.

Impact on workloads:
Based on function_references, this function is called from _compare_compatible and _compare_equal in specifier matching logic - potentially hot paths during dependency resolution where many version strings are compared. The test results show:

  • Pure numeric versions (e.g., "1.2.3", "1.2.3.4"): 18-35% faster - these benefit most since regex is completely avoided
  • Versions with many empty components (e.g., "...", 999 dots): 64-277% faster - empty strings bypass regex
  • Long numeric components (e.g., 999-digit strings): 187-587% faster - avoids regex on large strings
  • Versions with pre-release suffixes (e.g., "1.2a1", "1.2rc4"): Mostly neutral or slightly slower (2-10%) due to extra conditional checks, but the regex still runs when needed

The optimization trades a small overhead for suffix-heavy versions (where most components match the pattern) for substantial gains on common numeric-only versions. Given typical version strings in package management rarely have pre-release suffixes on every component, the overall impact is positive, as evidenced by the 37% average speedup.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 86 Passed
⏪ Replay Tests 255 Passed
🔎 Concolic Coverage Tests 1 Passed
📊 Tests Coverage 100.0%
🌀 Click to see Generated Regression Tests
import re

# imports
import pytest  # used for our unit tests
from src.packaging.specifiers import _version_split

_prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)")
from src.packaging.specifiers import _version_split

# unit tests

class TestVersionSplitBasic:
    def test_simple_numeric(self):
        # Basic version string with only numbers
        codeflash_output = _version_split("1.2.3") # 1.83μs -> 1.54μs (18.9% faster)

    def test_single_number(self):
        # Single number version
        codeflash_output = _version_split("42") # 1.33μs -> 1.21μs (10.3% faster)

    def test_leading_zero(self):
        # Leading zero in version number
        codeflash_output = _version_split("01.2") # 1.62μs -> 1.42μs (14.7% faster)

    def test_alpha_suffix(self):
        # Version with alpha suffix
        codeflash_output = _version_split("1.2a1") # 2.33μs -> 2.33μs (0.000% faster)

    def test_beta_suffix(self):
        # Version with beta suffix
        codeflash_output = _version_split("1.2b3") # 1.88μs -> 1.96μs (4.24% slower)

    def test_rc_suffix(self):
        # Version with release candidate suffix
        codeflash_output = _version_split("1.2rc4") # 1.92μs -> 1.96μs (2.09% slower)

    def test_multiple_suffixes(self):
        # Version with multiple suffixes
        codeflash_output = _version_split("1.2.3a4") # 2.04μs -> 2.04μs (0.000% faster)

    def test_c_suffix(self):
        # Version with 'c' suffix
        codeflash_output = _version_split("1.2c5") # 1.67μs -> 1.79μs (6.98% slower)

    def test_epoch(self):
        # Version string with epoch
        codeflash_output = _version_split("2!1.2.3") # 1.54μs -> 1.21μs (27.5% faster)

    def test_epoch_with_suffix(self):
        # Version string with epoch and suffix
        codeflash_output = _version_split("1!2.3b4") # 1.83μs -> 1.92μs (4.38% slower)

    def test_epoch_zero(self):
        # Version string with epoch zero
        codeflash_output = _version_split("0!1.2") # 1.29μs -> 1.08μs (19.3% faster)

    def test_empty_string(self):
        # Empty string should yield ["0", ""]
        codeflash_output = _version_split("") # 1.33μs -> 1.04μs (28.0% faster)

    def test_trailing_dot(self):
        # Trailing dot should result in an empty string at the end
        codeflash_output = _version_split("1.2.") # 1.38μs -> 1.12μs (22.2% faster)

    def test_leading_dot(self):
        # Leading dot should result in an empty string at the start
        codeflash_output = _version_split(".1.2") # 1.38μs -> 1.12μs (22.2% faster)

    def test_multiple_dots(self):
        # Multiple consecutive dots
        codeflash_output = _version_split("1..2") # 1.38μs -> 1.12μs (22.2% faster)

class TestVersionSplitEdge:
    def test_only_epoch(self):
        # Only epoch, no version part
        codeflash_output = _version_split("3!") # 1.17μs -> 958ns (21.8% faster)

    def test_only_suffix(self):
        # Only a suffix as the version
        codeflash_output = _version_split("a1") # 1.12μs -> 1.04μs (8.07% faster)

    def test_suffix_without_number(self):
        # Suffix without a number should not be split
        codeflash_output = _version_split("1.2a") # 1.42μs -> 1.50μs (5.53% slower)

    def test_suffix_with_multiple_letters(self):
        # Suffix with multiple letters not matching the regex
        codeflash_output = _version_split("1.2xyz3") # 1.67μs -> 1.71μs (2.40% slower)

    def test_multiple_exclamations(self):
        # Multiple exclamation marks (only last counts as epoch separator)
        codeflash_output = _version_split("1!2!3.4") # 1.38μs -> 1.17μs (17.9% faster)

    def test_dot_and_exclamation(self):
        # Exclamation at the end
        codeflash_output = _version_split("1.2!") # 1.21μs -> 875ns (38.1% faster)

    def test_version_with_spaces(self):
        # Spaces in version string (should be preserved)
        codeflash_output = _version_split("1. 2.3") # 1.46μs -> 1.25μs (16.6% faster)

    def test_version_with_hyphens(self):
        # Hyphens are not split, so should be preserved as-is
        codeflash_output = _version_split("1.2-rc1") # 1.50μs -> 1.58μs (5.24% slower)

    def test_version_with_leading_and_trailing_spaces(self):
        # Leading/trailing spaces are preserved
        codeflash_output = _version_split(" 1.2 ") # 1.46μs -> 1.50μs (2.80% slower)

    def test_version_with_empty_components(self):
        # Multiple empty components
        codeflash_output = _version_split("...") # 1.71μs -> 1.04μs (63.9% faster)

    def test_version_with_large_number(self):
        # Large number in version string
        codeflash_output = _version_split("12345678901234567890.1") # 2.12μs -> 1.33μs (59.4% faster)

    def test_non_ascii_characters(self):
        # Non-ASCII characters are preserved
        codeflash_output = _version_split("1.2.α") # 3.04μs -> 2.54μs (19.6% faster)

    def test_suffix_with_zero(self):
        # Suffix with zero (should match regex)
        codeflash_output = _version_split("1.2a0") # 1.79μs -> 1.92μs (6.52% slower)

    def test_suffix_with_rc_and_zero(self):
        codeflash_output = _version_split("1.2rc0") # 1.83μs -> 1.92μs (4.38% slower)

    def test_suffix_with_c_and_zero(self):
        codeflash_output = _version_split("1.2c0") # 1.71μs -> 1.79μs (4.69% slower)

    def test_suffix_with_b_and_zero(self):
        codeflash_output = _version_split("1.2b0") # 1.67μs -> 1.79μs (6.98% slower)

    def test_suffix_with_multiple_suffixes(self):
        # Suffixes not matching regex are not split
        codeflash_output = _version_split("1.2a1b2") # 1.54μs -> 1.67μs (7.56% slower)

    def test_suffix_with_multiple_rcs(self):
        codeflash_output = _version_split("1.2rc1rc2") # 1.58μs -> 1.75μs (9.54% slower)

    def test_epoch_with_multiple_dots(self):
        codeflash_output = _version_split("5!1..2") # 1.50μs -> 1.17μs (28.5% faster)

class TestVersionSplitLargeScale:
    def test_many_components(self):
        # Large number of version components
        version = ".".join(str(i) for i in range(1000))
        expected = ["0"] + [str(i) for i in range(1000)]
        codeflash_output = _version_split(version) # 202μs -> 84.7μs (140% faster)

    def test_many_alpha_suffixes(self):
        # Large number of components with alpha suffixes
        version = ".".join(f"{i}a{i}" for i in range(1000))
        expected = ["0"] + [str(i) for i in range(1000)] + [f"a{i}" for i in range(1000)]
        # The function splits each 'XaX' into ['X', 'aX']
        codeflash_output = _version_split(version) # 303μs -> 339μs (10.8% slower)

    def test_long_epoch(self):
        # Large epoch value with multiple components
        version = "123456789!" + ".".join(str(i) for i in range(10))
        expected = ["123456789"] + [str(i) for i in range(10)]
        codeflash_output = _version_split(version) # 2.67μs -> 1.79μs (48.8% faster)

    def test_long_component(self):
        # A very long component string
        long_component = "1" * 500
        version = f"{long_component}.{long_component}"
        expected = ["0", long_component, long_component]
        codeflash_output = _version_split(version) # 23.5μs -> 3.42μs (587% faster)

    def test_mixed_large(self):
        # Large version string with mixed numeric and suffixes
        version = ".".join([f"{i}rc{i}" if i % 2 == 0 else f"{i}b{i}" for i in range(500)])
        expected = ["0"] + [str(i) for i in range(500)] + [("rc" if i % 2 == 0 else "b") + str(i) for i in range(500)]
        codeflash_output = _version_split(version) # 156μs -> 179μs (13.1% slower)

    def test_all_empty_components(self):
        # All empty components (dots only)
        version = "." * 999  # 1000 empty components
        expected = ["0"] + [""] * 1000
        codeflash_output = _version_split(version) # 119μs -> 31.8μs (277% faster)

    def test_performance_large(self):
        # Performance test: Should not take too long
        version = ".".join(str(i) for i in range(999))
        codeflash_output = _version_split(version); result = codeflash_output # 206μs -> 71.0μs (191% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import re

# imports
import pytest  # used for our unit tests
from src.packaging.specifiers import _version_split

_prefix_regex = re.compile(r"([0-9]+)((?:a|b|c|rc)[0-9]+)")
from src.packaging.specifiers import _version_split

# unit tests

# -----------------------
# Basic Test Cases
# -----------------------

def test_basic_no_epoch():
    # No epoch, simple version
    codeflash_output = _version_split("1.2.3") # 1.33μs -> 1.08μs (23.0% faster)

def test_basic_with_epoch():
    # Version with epoch
    codeflash_output = _version_split("2!1.2.3") # 1.33μs -> 1.12μs (18.5% faster)

def test_basic_with_alpha():
    # Version with alpha pre-release
    codeflash_output = _version_split("1.2a1") # 1.83μs -> 1.88μs (2.24% slower)

def test_basic_with_beta():
    # Version with beta pre-release
    codeflash_output = _version_split("1.2b3") # 1.75μs -> 1.75μs (0.000% faster)

def test_basic_with_rc():
    # Version with release candidate
    codeflash_output = _version_split("1.2rc5") # 1.79μs -> 1.83μs (2.29% slower)

def test_basic_with_multiple_components():
    # Version with multiple numeric components
    codeflash_output = _version_split("1.2.3.4") # 1.46μs -> 1.08μs (34.5% faster)

def test_basic_with_c():
    # Version with 'c' pre-release
    codeflash_output = _version_split("1.2c4") # 1.67μs -> 1.79μs (6.98% slower)

def test_basic_with_zero_epoch():
    # Version with explicit zero epoch
    codeflash_output = _version_split("0!1.2.3") # 1.29μs -> 1.12μs (14.8% faster)

# -----------------------
# Edge Test Cases
# -----------------------

def test_edge_empty_string():
    # Empty string should result in ["0", ""]
    codeflash_output = _version_split("") # 1.17μs -> 917ns (27.3% faster)

def test_edge_only_epoch():
    # Only epoch, no version
    codeflash_output = _version_split("5!") # 1.04μs -> 875ns (19.1% faster)

def test_edge_only_dot():
    # Only dot as version
    codeflash_output = _version_split(".") # 1.29μs -> 917ns (40.9% faster)

def test_edge_leading_trailing_dots():
    # Leading and trailing dots
    codeflash_output = _version_split(".1.2.") # 1.58μs -> 1.25μs (26.6% faster)

def test_edge_multiple_exclamations():
    # Multiple exclamation marks, only the last one counts as epoch separator
    codeflash_output = _version_split("3!2!1.2") # 1.33μs -> 1.12μs (18.5% faster)

def test_edge_non_numeric_epoch():
    # Non-numeric epoch should be treated as part of the version
    codeflash_output = _version_split("a!1.2") # 1.21μs -> 1.08μs (11.5% faster)

def test_edge_non_numeric_components():
    # Non-numeric version components
    codeflash_output = _version_split("foo.bar") # 1.42μs -> 1.17μs (21.5% faster)

def test_edge_mixed_numeric_and_alpha():
    # Mixed numeric and alpha in one component
    codeflash_output = _version_split("1.2a1.3b2") # 2.62μs -> 2.50μs (5.00% faster)

def test_edge_component_with_multiple_letters():
    # Component with multiple letters not matching the regex
    codeflash_output = _version_split("1.2abc3") # 1.46μs -> 1.58μs (7.95% slower)

def test_edge_component_with_rc_and_number():
    # rc followed by a number
    codeflash_output = _version_split("1.2rc10") # 1.75μs -> 1.88μs (6.67% slower)

def test_edge_component_with_rc_and_no_number():
    # rc with no number should not match the regex
    codeflash_output = _version_split("1.2rc") # 1.50μs -> 1.54μs (2.66% slower)

def test_edge_component_with_a0():
    # 'a0' is a valid pre-release
    codeflash_output = _version_split("1.2a0") # 1.67μs -> 1.71μs (2.46% slower)

def test_edge_component_with_b0():
    # 'b0' is a valid pre-release
    codeflash_output = _version_split("1.2b0") # 1.67μs -> 1.83μs (9.11% slower)

def test_edge_component_with_c0():
    # 'c0' is a valid pre-release
    codeflash_output = _version_split("1.2c0") # 1.58μs -> 1.75μs (9.54% slower)

def test_edge_component_with_rc0():
    # 'rc0' is a valid pre-release
    codeflash_output = _version_split("1.2rc0") # 1.75μs -> 1.79μs (2.29% slower)

def test_edge_component_with_leading_zeros():
    # Leading zeros in version numbers
    codeflash_output = _version_split("01.002.0003") # 1.83μs -> 1.33μs (37.5% faster)

def test_edge_component_with_hyphen():
    # Hyphens are not split, treated as part of the component
    codeflash_output = _version_split("1.2-3") # 1.25μs -> 1.50μs (16.7% slower)

def test_edge_component_with_underscore():
    # Underscores are not split, treated as part of the component
    codeflash_output = _version_split("1.2_3") # 1.29μs -> 1.46μs (11.4% slower)

def test_edge_component_with_plus():
    # Plus is not split, treated as part of the component
    codeflash_output = _version_split("1.2+3") # 1.38μs -> 1.50μs (8.33% slower)

def test_edge_component_with_spaces():
    # Spaces are not split, treated as part of the component
    codeflash_output = _version_split("1. 2 .3") # 1.50μs -> 1.25μs (20.0% faster)

def test_edge_component_with_empty_between_dots():
    # Multiple consecutive dots create empty components
    codeflash_output = _version_split("1..2") # 1.29μs -> 1.08μs (19.1% faster)

def test_edge_component_with_dot_at_end():
    # Dot at the end creates empty component
    codeflash_output = _version_split("1.2.") # 1.29μs -> 1.12μs (14.8% faster)

def test_edge_component_with_dot_at_start():
    # Dot at the start creates empty component
    codeflash_output = _version_split(".1.2") # 1.25μs -> 1.08μs (15.3% faster)

def test_edge_component_with_long_component():
    # Long component string
    long_comp = "1" * 100
    codeflash_output = _version_split(long_comp) # 3.71μs -> 1.29μs (187% faster)

def test_edge_component_with_non_ascii():
    # Non-ascii characters should be preserved
    codeflash_output = _version_split("1.β.2") # 2.17μs -> 1.96μs (10.6% faster)

def test_edge_component_with_mixed_case():
    # Case sensitivity is preserved
    codeflash_output = _version_split("1.A2.B3") # 1.29μs -> 1.29μs (0.000% faster)

def test_edge_component_with_malformed_pre():
    # Malformed pre-release (e.g. a, b, c, rc with no number)
    codeflash_output = _version_split("1.2a") # 1.25μs -> 1.46μs (14.3% slower)
    codeflash_output = _version_split("1.2b") # 709ns -> 708ns (0.141% faster)
    codeflash_output = _version_split("1.2c") # 542ns -> 541ns (0.185% faster)
    codeflash_output = _version_split("1.2rc") # 792ns -> 792ns (0.000% faster)

def test_edge_component_with_multiple_letters_and_numbers():
    # Component with letters and numbers not matching the regex
    codeflash_output = _version_split("1.2abc123") # 1.46μs -> 1.62μs (10.2% slower)

# -----------------------
# Large Scale Test Cases
# -----------------------

def test_large_many_components():
    # Version string with 1000 components
    version = ".".join(str(i) for i in range(1000))
    expected = ["0"] + [str(i) for i in range(1000)]
    codeflash_output = _version_split(version) # 193μs -> 72.7μs (166% faster)

def test_large_many_alpha_components():
    # Version string with 500 numeric and 500 alpha components
    version = ".".join([f"{i}a{i}" for i in range(500)] + [f"{i}b{i}" for i in range(500)])
    expected = ["0"]
    for i in range(500):
        expected.extend([str(i), f"a{i}"])
    for i in range(500):
        expected.extend([f"{i}b{i}"])
    codeflash_output = _version_split(version) # 274μs -> 311μs (11.9% slower)

def test_large_epoch_and_components():
    # Large epoch and many components
    version = "123456789!" + ".".join(str(i) for i in range(100))
    expected = ["123456789"] + [str(i) for i in range(100)]
    codeflash_output = _version_split(version) # 14.8μs -> 9.00μs (64.4% faster)

def test_large_component_length():
    # Very long single component
    long_component = "9" * 999
    codeflash_output = _version_split(long_component) # 21.2μs -> 3.17μs (570% faster)

def test_large_with_mixed_pre_releases():
    # Mix of numeric and pre-release components
    version = ".".join([f"{i}a{i}" if i % 2 == 0 else f"{i}b{i}" for i in range(100)])
    expected = ["0"]
    for i in range(100):
        if i % 2 == 0:
            expected.extend([str(i), f"a{i}"])
        else:
            expected.extend([str(i), f"b{i}"])
    codeflash_output = _version_split(version) # 30.7μs -> 33.5μs (8.34% slower)

def test_large_with_many_empty_components():
    # Many empty components (dots)
    version = "." * 999
    expected = ["0"] + [""] * 1000
    codeflash_output = _version_split(version) # 105μs -> 31.9μs (230% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from src.packaging.specifiers import _version_split

def test__version_split():
    _version_split('0c0.')
⏪ Click to see Replay Tests
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
test_benchmark_py__replay_test_0.py::test_src_packaging_specifiers__version_split 158μs 118μs 33.9%✅
🔎 Click to see Concolic Coverage Tests
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
codeflash_concolic_ui1l843q/tmp13d42exe/test_concolic_coverage.py::test__version_split 3.46μs 3.92μs -11.7%⚠️

To edit these changes git checkout codeflash/optimize-_version_split-mjjlqufy and push.

Codeflash Static Badge

The optimization adds a **pre-filter check** before invoking the expensive regex operation, avoiding regex overhead for components that cannot possibly match the pattern.

**What changed:**
The code now checks `if item and item[0].isdigit() and not item.isdigit()` before calling `_prefix_regex.fullmatch(item)`. This condition identifies items that:
1. Are non-empty
2. Start with a digit
3. Are NOT purely numeric (meaning they contain mixed alphanumeric content)

Only these candidates can match the pattern `([0-9]+)((?:a|b|c|rc)[0-9]+)` (e.g., "1a2", "3rc5"). The optimization skips regex matching for purely numeric items (like "1", "2", "3") and non-numeric items (like "foo", ""), which comprise the majority of version components in typical usage.

**Why it's faster:**
Regex operations in Python have significant overhead - even for non-matches, the engine must parse and evaluate the pattern. The line profiler shows:
- **Original**: `_prefix_regex.fullmatch(item)` was called 10,411 times, consuming 5.2 million nanoseconds (25.8% of total time)
- **Optimized**: The regex is now called only 2,638 times (a 75% reduction), consuming just 1.2 million nanoseconds (8% of total time)

The fast-path checks (`item[0].isdigit()` and `not item.isdigit()`) are native string operations implemented in C, making them orders of magnitude faster than regex compilation and matching.

**Impact on workloads:**
Based on `function_references`, this function is called from `_compare_compatible` and `_compare_equal` in specifier matching logic - potentially hot paths during dependency resolution where many version strings are compared. The test results show:

- **Pure numeric versions** (e.g., "1.2.3", "1.2.3.4"): 18-35% faster - these benefit most since regex is completely avoided
- **Versions with many empty components** (e.g., "...", 999 dots): 64-277% faster - empty strings bypass regex
- **Long numeric components** (e.g., 999-digit strings): 187-587% faster - avoids regex on large strings
- **Versions with pre-release suffixes** (e.g., "1.2a1", "1.2rc4"): Mostly neutral or slightly slower (2-10%) due to extra conditional checks, but the regex still runs when needed

The optimization trades a small overhead for suffix-heavy versions (where most components match the pattern) for substantial gains on common numeric-only versions. Given typical version strings in package management rarely have pre-release suffixes on every component, the overall impact is positive, as evidenced by the 37% average speedup.
@codeflash-ai codeflash-ai Bot requested a review from KRRT7 December 24, 2025 05:57
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Dec 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants