Skip to content

⚡️ Speed up function _str_to_version_tuple by 26%#113

Open
codeflash-ai[bot] wants to merge 1 commit intomainfrom
codeflash/optimize-_str_to_version_tuple-mlcdu11b
Open

⚡️ Speed up function _str_to_version_tuple by 26%#113
codeflash-ai[bot] wants to merge 1 commit intomainfrom
codeflash/optimize-_str_to_version_tuple-mlcdu11b

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Feb 7, 2026

📄 26% (0.26x) speedup for _str_to_version_tuple in src/datasets/utils/version.py

⏱️ Runtime : 428 microseconds 340 microseconds (best of 78 runs)

📝 Explanation and details

The optimized code achieves a 26% runtime improvement (428μs → 340μs) by eliminating expensive operations in the version tuple construction path.

Key Optimizations

1. Direct tuple construction instead of generator expression

  • Original: tuple(int(v) for v in [res.group("major"), res.group("minor"), res.group("patch")])
  • Optimized: _int(major_s), _int(minor_s), _int(patch_s) = res.groups() followed by return (_int(major_s), _int(minor_s), _int(patch_s))

This change eliminates:

  • Generator object creation and iteration overhead
  • Three separate res.group() calls (replaced with single res.groups() call)
  • List allocation for the three group names
  • Generator-to-tuple conversion overhead

2. Local binding of int builtin

  • _int = int caches the global int lookup as a local variable
  • Python resolves local variables faster than global builtins (LOAD_FAST vs LOAD_GLOBAL bytecode)
  • Saves 3 global lookups per function call

Performance Impact

The line profiler shows the critical line improvement:

  • Original: 1.65ms (62.8% of total time) for the tuple generation line
  • Optimized: 0.77ms (combined time for groups() + tuple construction, ~45% of total time)

Test Results Analysis

The optimization shows consistent 40-70% speedup across valid version parsing cases:

  • Simple versions like "1.2.3": 53-54% faster (5.5μs → 3.6μs)
  • Edge cases with zeros/leading zeros: 46-65% faster
  • Large numbers: 41-46% faster
  • Invalid inputs: Minimal overhead, ~0-4% faster (error path unchanged)

Impact on Workloads

Based on function_references, this function is called in __post_init__ of what appears to be a Version class. This means:

  • Every version object instantiation will benefit from this 26% speedup
  • In dataset loading workflows with many version checks, this compounds significantly
  • The optimization is particularly valuable if version parsing occurs in loops or bulk operations (as suggested by the 300-item test case showing consistent gains)

The optimization maintains identical behavior for all inputs while substantially reducing execution time for the common case of valid version strings, with negligible impact on error cases.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 222 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Click to see Generated Regression Tests
import re  # required by the function under test

# imports
import pytest  # used for our unit tests
from src.datasets.utils.version import _str_to_version_tuple

def test_basic_valid_versions():
    # Simple canonical version
    codeflash_output = _str_to_version_tuple("1.2.3") # 5.46μs -> 3.57μs (53.0% faster)
    # All zeros should map to (0, 0, 0)
    codeflash_output = _str_to_version_tuple("0.0.0") # 2.11μs -> 1.28μs (65.4% faster)
    # Leading zeros are allowed by the regex and should be converted to ints
    codeflash_output = _str_to_version_tuple("01.002.0003") # 2.01μs -> 1.40μs (44.0% faster)
    # Larger major/minor/patch numbers are supported and returned as ints
    codeflash_output = _str_to_version_tuple("10.0.1") # 1.61μs -> 1.02μs (58.4% faster)

@pytest.mark.parametrize(
    "bad_version",
    [
        "",  # empty string
        "1.2",  # too few components
        "1",  # single component
        "1.2.3.4",  # too many components
        "1..3",  # missing minor
        ".1.2",  # leading dot
        "1.2.",  # trailing dot
        "a.b.c",  # non-digit components
        "1.2b.3",  # letters mixed with digits
        "v1.2.3",  # prefix letter
        "1.2.3-alpha",  # semantic suffix not allowed
        " 1.2.3",  # leading whitespace (regex anchored to ^ prohibits this)
        "1.2.3 ",  # trailing whitespace
        "\n1.2.3",  # newline prefix
    ],
)
def test_invalid_formats_raise_value_error(bad_version):
    # All of these malformed strings must raise ValueError with the specific message pattern
    with pytest.raises(ValueError) as excinfo:
        _str_to_version_tuple(bad_version) # 39.9μs -> 39.0μs (2.42% faster)
    # Check that the exception message mentions the invalid version and format guidance
    msg = str(excinfo.value)

@pytest.mark.parametrize("non_str", [None, 123, 1.2, b"1.2.3", object()])
def test_non_string_inputs_raise_type_error(non_str):
    # Passing non-string-like objects to re.Pattern.match with a string pattern raises TypeError
    with pytest.raises(TypeError):
        _str_to_version_tuple(non_str) # 11.4μs -> 10.9μs (4.39% faster)

def test_large_and_leading_zero_numbers_convert_to_ints():
    # Very large numeric components (still within Python int capability) are supported
    big_major = "123456789012345678901234567890"
    big_minor = "98765432109876543210987654321"
    s = f"{big_major}.{big_minor}.0"
    codeflash_output = _str_to_version_tuple(s); result = codeflash_output # 6.43μs -> 4.54μs (41.6% faster)

    # Extremely padded numbers with many leading zeros should normalize to the same integers
    padded = "0000000000000000000001.00002.000003"
    codeflash_output = _str_to_version_tuple(padded) # 2.62μs -> 1.79μs (46.8% faster)

def test_many_valid_versions_large_scale():
    # Create a deterministic collection of valid version strings (300 items to stay well below 1000)
    count = 300
    versions = [f"{i}.{i+1}.{i+2}" for i in range(count)]
    # Compute expected tuples deterministically
    expected = [(i, i + 1, i + 2) for i in range(count)]
    # Convert all versions using the function under test
    results = [_str_to_version_tuple(v) for v in versions]

def test_partial_match_is_not_allowed_and_whitespace_matters():
    # A string containing a valid version but with surrounding text should not match
    with pytest.raises(ValueError):
        _str_to_version_tuple("prefix1.2.3") # 3.06μs -> 2.96μs (3.48% faster)
    with pytest.raises(ValueError):
        _str_to_version_tuple("1.2.3suffix") # 1.51μs -> 1.50μs (0.133% faster)
    # Internal whitespace breaks the strict x.y.z pattern
    with pytest.raises(ValueError):
        _str_to_version_tuple("1. 2.3") # 1.05μs -> 1.07μs (1.87% slower)
    with pytest.raises(ValueError):
        _str_to_version_tuple("1.2 .3") # 919ns -> 899ns (2.22% faster)

def test_variations_with_zeros_and_small_numbers():
    codeflash_output = _str_to_version_tuple("00.00.01") # 5.83μs -> 3.94μs (47.9% faster)
    codeflash_output = _str_to_version_tuple("0.01.10") # 2.27μs -> 1.49μs (52.5% faster)
    codeflash_output = _str_to_version_tuple("007.0008.00009") # 1.97μs -> 1.32μs (49.8% 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

import pytest
from src.datasets.utils.version import _str_to_version_tuple

def test_basic_valid_version_simple():
    """Test parsing a simple valid version string in x.y.z format."""
    codeflash_output = _str_to_version_tuple("1.2.3"); result = codeflash_output # 5.50μs -> 3.56μs (54.5% faster)

def test_basic_valid_version_zeros():
    """Test parsing a valid version string with zeros."""
    codeflash_output = _str_to_version_tuple("0.0.0"); result = codeflash_output # 5.42μs -> 3.69μs (46.7% faster)

def test_basic_valid_version_large_numbers():
    """Test parsing a valid version string with larger numbers."""
    codeflash_output = _str_to_version_tuple("10.20.30"); result = codeflash_output # 5.69μs -> 3.83μs (48.7% faster)

def test_basic_valid_version_mixed_sizes():
    """Test parsing a valid version string with numbers of different magnitudes."""
    codeflash_output = _str_to_version_tuple("1.100.5"); result = codeflash_output # 5.73μs -> 3.80μs (50.9% faster)

def test_basic_return_type_is_tuple():
    """Test that the return type is always a tuple."""
    codeflash_output = _str_to_version_tuple("5.6.7"); result = codeflash_output # 5.50μs -> 3.62μs (52.0% faster)

def test_basic_tuple_elements_are_integers():
    """Test that all elements in the returned tuple are integers."""
    codeflash_output = _str_to_version_tuple("2.3.4"); result = codeflash_output # 5.42μs -> 3.60μs (50.7% faster)

def test_edge_missing_major_version():
    """Test that missing major version raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple(".2.3") # 2.43μs -> 2.45μs (0.817% slower)

def test_edge_missing_minor_version():
    """Test that missing minor version raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1..3") # 2.46μs -> 2.38μs (3.62% faster)

def test_edge_missing_patch_version():
    """Test that missing patch version raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2.") # 2.40μs -> 2.36μs (1.99% faster)

def test_edge_only_dots():
    """Test that string with only dots raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("...") # 2.41μs -> 2.34μs (2.99% faster)

def test_edge_empty_string():
    """Test that empty string raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("") # 2.40μs -> 2.39μs (0.712% faster)

def test_edge_too_many_parts():
    """Test that version string with more than 3 parts raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2.3.4") # 2.97μs -> 2.99μs (0.669% slower)

def test_edge_too_few_parts():
    """Test that version string with fewer than 3 parts raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2") # 2.28μs -> 2.27μs (0.132% faster)

def test_edge_single_number():
    """Test that single number without dots raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("123") # 2.39μs -> 2.32μs (3.15% faster)

def test_edge_non_numeric_characters():
    """Test that non-numeric characters in version parts raise ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.a.3") # 3.04μs -> 2.79μs (8.66% faster)

def test_edge_alphabetic_prefix():
    """Test that alphabetic prefix raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("v1.2.3") # 2.86μs -> 2.76μs (3.62% faster)

def test_edge_alphabetic_suffix():
    """Test that alphabetic suffix raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2.3a") # 3.01μs -> 3.05μs (1.31% slower)

def test_edge_leading_zero_in_single_digit():
    """Test that leading zeros are still valid (they form valid integers)."""
    codeflash_output = _str_to_version_tuple("01.02.03"); result = codeflash_output # 5.55μs -> 3.76μs (47.7% faster)

def test_edge_negative_numbers():
    """Test that negative numbers raise ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("-1.2.3") # 2.71μs -> 2.61μs (3.83% faster)

def test_edge_negative_minor():
    """Test that negative minor version raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.-2.3") # 2.94μs -> 2.93μs (0.307% faster)

def test_edge_whitespace_in_version():
    """Test that whitespace in version string raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1. 2.3") # 3.02μs -> 2.93μs (3.11% faster)

def test_edge_leading_whitespace():
    """Test that leading whitespace raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple(" 1.2.3") # 2.82μs -> 2.79μs (1.04% faster)

def test_edge_trailing_whitespace():
    """Test that trailing whitespace raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2.3 ") # 3.08μs -> 3.00μs (2.76% faster)

def test_edge_comma_separator():
    """Test that comma separators raise ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1,2,3") # 2.90μs -> 2.83μs (2.33% faster)

def test_edge_special_characters():
    """Test that special characters raise ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1@2.3") # 2.90μs -> 2.83μs (2.26% faster)

def test_edge_plus_sign():
    """Test that plus sign raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("+1.2.3") # 2.86μs -> 2.78μs (2.99% faster)

def test_edge_float_notation():
    """Test that float notation raises ValueError."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2e3.4") # 3.08μs -> 3.00μs (2.70% faster)

def test_edge_very_large_version_numbers():
    """Test parsing version string with very large numbers."""
    codeflash_output = _str_to_version_tuple("999999999.999999999.999999999"); result = codeflash_output # 6.24μs -> 4.26μs (46.4% faster)

def test_edge_single_digit_all_max():
    """Test parsing single digit version numbers."""
    codeflash_output = _str_to_version_tuple("9.9.9"); result = codeflash_output # 5.53μs -> 3.75μs (47.6% faster)

def test_edge_mixed_single_and_multi_digit():
    """Test parsing version with mixed digit counts."""
    codeflash_output = _str_to_version_tuple("1.22.333"); result = codeflash_output # 5.54μs -> 3.81μs (45.5% faster)

def test_edge_asymmetric_large_numbers():
    """Test parsing asymmetric large version numbers."""
    codeflash_output = _str_to_version_tuple("1.999999.2"); result = codeflash_output # 5.53μs -> 3.75μs (47.6% faster)

def test_edge_error_message_contains_version_string():
    """Test that error message includes the invalid version string."""
    invalid_version = "invalid.version.string"
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple(invalid_version) # 2.79μs -> 2.68μs (4.18% faster)

def test_edge_error_message_mentions_format():
    """Test that error message mentions the expected format."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("1.2") # 2.44μs -> 2.40μs (1.67% faster)
    error_msg = str(exc_info.value)

def test_edge_error_message_mentions_digits():
    """Test that error message mentions digit requirement."""
    with pytest.raises(ValueError) as exc_info:
        _str_to_version_tuple("a.b.c") # 2.85μs -> 2.85μs (0.140% faster)
    error_msg = str(exc_info.value)

def test_large_scale_multiple_valid_versions():
    """Test parsing multiple valid version strings in sequence."""
    versions = [
        ("0.0.1", (0, 0, 1)),
        ("1.0.0", (1, 0, 0)),
        ("1.2.3", (1, 2, 3)),
        ("10.20.30", (10, 20, 30)),
        ("100.200.300", (100, 200, 300)),
        ("999.888.777", (999, 888, 777)),
        ("123.456.789", (123, 456, 789)),
        ("5.4.3", (5, 4, 3)),
    ]
    # Test each version and ensure correct parsing
    for version_str, expected in versions:
        codeflash_output = _str_to_version_tuple(version_str); result = codeflash_output # 17.8μs -> 11.6μs (54.1% faster)

def test_large_scale_many_invalid_versions():
    """Test that multiple invalid versions all raise ValueError appropriately."""
    invalid_versions = [
        "1",
        "1.2",
        "1.2.3.4",
        "a.b.c",
        "1.a.3",
        "-1.2.3",
        "1.-2.3",
        "1.2.-3",
        "1 .2.3",
        "1. 2.3",
        "1.2 .3",
        ".1.2.3",
        "1..2.3",
        "1.2..3",
        "v1.2.3",
        "1.2.3v",
    ]
    # Test that each invalid version raises ValueError
    for invalid_version in invalid_versions:
        with pytest.raises(ValueError):
            _str_to_version_tuple(invalid_version)

def test_large_scale_boundary_increments():
    """Test parsing versions with systematic increments."""
    # Test multiple versions with incremental numbers
    results = []
    for i in range(1, 11):
        version_str = f"{i}.{i}.{i}"
        codeflash_output = _str_to_version_tuple(version_str); result = codeflash_output # 19.4μs -> 12.6μs (53.7% faster)
        results.append(result)

def test_large_scale_varying_magnitudes():
    """Test parsing versions with various magnitude combinations."""
    test_cases = []
    # Generate test cases with different magnitudes
    for major in [0, 1, 10, 100]:
        for minor in [0, 1, 10, 100]:
            for patch in [0, 1, 10, 100]:
                version_str = f"{major}.{minor}.{patch}"
                codeflash_output = _str_to_version_tuple(version_str); result = codeflash_output
                test_cases.append(version_str)

def test_large_scale_consistent_parsing():
    """Test that parsing the same version multiple times gives consistent results."""
    version_str = "42.17.98"
    results = [_str_to_version_tuple(version_str) for _ in range(100)]

def test_large_scale_error_consistency():
    """Test that invalid versions consistently raise ValueError."""
    invalid_version = "not.a.valid.version"
    # Test that the same invalid version always raises ValueError
    for _ in range(50):
        with pytest.raises(ValueError):
            _str_to_version_tuple(invalid_version)

def test_large_scale_tuple_unpacking():
    """Test that returned tuples can be unpacked and used correctly."""
    version_str = "3.14.159"
    major, minor, patch = _str_to_version_tuple(version_str) # 5.82μs -> 4.00μs (45.3% faster)

def test_large_scale_tuple_indexing():
    """Test that returned tuples support indexing."""
    codeflash_output = _str_to_version_tuple("2.5.8"); result = codeflash_output # 5.39μs -> 3.68μs (46.6% faster)

def test_large_scale_tuple_immutability():
    """Test that returned tuples are immutable."""
    codeflash_output = _str_to_version_tuple("1.2.3"); result = codeflash_output # 5.28μs -> 3.56μs (48.0% faster)
    # Attempting to modify tuple should raise TypeError
    with pytest.raises(TypeError):
        result[0] = 5

def test_large_scale_tuple_comparison():
    """Test that tuples can be compared correctly."""
    codeflash_output = _str_to_version_tuple("1.2.3"); v1 = codeflash_output # 5.32μs -> 3.59μs (48.1% faster)
    codeflash_output = _str_to_version_tuple("1.2.3"); v2 = codeflash_output # 2.01μs -> 1.18μs (70.4% faster)
    codeflash_output = _str_to_version_tuple("1.2.4"); v3 = codeflash_output # 1.59μs -> 993ns (60.1% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-_str_to_version_tuple-mlcdu11b and push.

Codeflash Static Badge

The optimized code achieves a **26% runtime improvement** (428μs → 340μs) by eliminating expensive operations in the version tuple construction path.

## Key Optimizations

**1. Direct tuple construction instead of generator expression**
- **Original**: `tuple(int(v) for v in [res.group("major"), res.group("minor"), res.group("patch")])`
- **Optimized**: `_int(major_s), _int(minor_s), _int(patch_s) = res.groups()` followed by `return (_int(major_s), _int(minor_s), _int(patch_s))`

This change eliminates:
- Generator object creation and iteration overhead
- Three separate `res.group()` calls (replaced with single `res.groups()` call)
- List allocation for the three group names
- Generator-to-tuple conversion overhead

**2. Local binding of `int` builtin**
- `_int = int` caches the global `int` lookup as a local variable
- Python resolves local variables faster than global builtins (LOAD_FAST vs LOAD_GLOBAL bytecode)
- Saves 3 global lookups per function call

## Performance Impact

The line profiler shows the critical line improvement:
- **Original**: 1.65ms (62.8% of total time) for the tuple generation line
- **Optimized**: 0.77ms (combined time for groups() + tuple construction, ~45% of total time)

## Test Results Analysis

The optimization shows **consistent 40-70% speedup** across valid version parsing cases:
- Simple versions like "1.2.3": **53-54% faster** (5.5μs → 3.6μs)
- Edge cases with zeros/leading zeros: **46-65% faster**
- Large numbers: **41-46% faster**
- Invalid inputs: Minimal overhead, ~0-4% faster (error path unchanged)

## Impact on Workloads

Based on `function_references`, this function is called in `__post_init__` of what appears to be a Version class. This means:
- **Every version object instantiation** will benefit from this 26% speedup
- In dataset loading workflows with many version checks, this compounds significantly
- The optimization is particularly valuable if version parsing occurs in loops or bulk operations (as suggested by the 300-item test case showing consistent gains)

The optimization maintains identical behavior for all inputs while substantially reducing execution time for the common case of valid version strings, with negligible impact on error cases.
@codeflash-ai codeflash-ai bot requested a review from aseembits93 February 7, 2026 14:01
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Feb 7, 2026
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