本案例基於 .claude/lib/markdown_link_checker.py 的實際程式碼,展示如何用 cProfile 和 line_profiler 進行效能分析。

先備知識

問題背景

現有設計

markdown_link_checker.py 是一個 Markdown 連結檢查工具,核心功能是解析文件中的連結並驗證其有效性。以下是關鍵的解析方法:

 1import re
 2from typing import List, Dict
 3
 4class MarkdownLinkChecker:
 5    """Markdown link checker with precompiled regex patterns"""
 6
 7    # Precompiled regex patterns at class level (good practice!)
 8    INLINE_LINK_PATTERN = re.compile(
 9        r'(?<!!)\[([^\]]+)\]\(([^)]+)\)'
10    )
11
12    REFERENCE_DEF_PATTERN = re.compile(
13        r'^\s*\[([^\]]+)\]:\s*(.+)$',
14        re.MULTILINE
15    )
16
17    REFERENCE_USE_PATTERN = re.compile(
18        r'\[([^\]]+)\]\[([^\]]+)\]'
19    )
20
21    def parse_markdown_links(self, content: str) -> List[Dict]:
22        """
23        Parse all links in Markdown content
24
25        Args:
26            content: Markdown content string
27
28        Returns:
29            list[dict]: List of links with text, target, line
30        """
31        links = []
32        lines = content.split('\n')
33
34        # First, collect reference-style link definitions
35        reference_defs = {}
36        for match in self.REFERENCE_DEF_PATTERN.finditer(content):
37            ref_name = match.group(1).lower()
38            ref_target = match.group(2).strip()
39            reference_defs[ref_name] = ref_target
40
41        # Track code block state
42        in_code_block = False
43
44        # Parse inline links line by line
45        for line_num, line in enumerate(lines, start=1):
46            # Check for code block boundaries
47            if line.strip().startswith("```"):
48                in_code_block = not in_code_block
49                continue
50
51            # Skip links inside code blocks
52            if in_code_block:
53                continue
54
55            # Inline links: [text](/python-advanced/04-cpython-internals/case-studies/profiling/target)
56            for match in self.INLINE_LINK_PATTERN.finditer(line):
57                links.append({
58                    "text": match.group(1),
59                    "target": match.group(2),
60                    "line": line_num
61                })
62
63            # Reference-style links: [text][ref]
64            for match in self.REFERENCE_USE_PATTERN.finditer(line):
65                ref_name = match.group(2).lower()
66                if ref_name in reference_defs:
67                    links.append({
68                        "text": match.group(1),
69                        "target": reference_defs[ref_name],
70                        "line": line_num
71                    })
72
73        return links

效能問題

處理大型文件時可能出現:

  • 正則表達式效率問題:複雜的 pattern 可能導致回溯
  • 重複編譯正則表達式:若 pattern 在方法內定義,每次呼叫都會重新編譯
  • 不必要的字串操作split() 會建立新的字串列表
  • 多次遍歷:分別處理引用定義和行內連結

進階解決方案

分析目標

  1. 找出效能瓶頸所在
  2. 量化各部分的時間消耗
  3. 驗證優化效果

實作步驟

步驟 1:使用 cProfile 進行函式級分析

cProfile 是 Python 標準庫的效能分析工具,可以測量每個函式的呼叫次數和執行時間。

 1import cProfile
 2import pstats
 3from io import StringIO
 4from pathlib import Path
 5
 6def profile_link_checker():
 7    """Profile the markdown link checker with cProfile"""
 8
 9    # Create test content with many links
10    test_content = generate_test_content(num_links=1000)
11
12    checker = MarkdownLinkChecker()
13
14    # Create profiler
15    profiler = cProfile.Profile()
16
17    # Run profiling
18    profiler.enable()
19    for _ in range(100):  # Run multiple times for better statistics
20        checker.parse_markdown_links(test_content)
21    profiler.disable()
22
23    # Analyze results
24    stream = StringIO()
25    stats = pstats.Stats(profiler, stream=stream)
26    stats.sort_stats('cumulative')  # Sort by cumulative time
27    stats.print_stats(20)  # Show top 20 functions
28
29    print(stream.getvalue())
30
31    return stats
32
33def generate_test_content(num_links: int) -> str:
34    """Generate test Markdown content with specified number of links"""
35    lines = ["# Test Document\n"]
36
37    for i in range(num_links):
38        if i % 3 == 0:
39            # Inline link
40            lines.append(f"Check out [Link {i}](https://example.com/{i})\n")
41        elif i % 3 == 1:
42            # Reference-style link
43            lines.append(f"See [Reference {i}][ref{i}]\n")
44        else:
45            # Plain text with potential regex traps
46            lines.append(f"This is paragraph {i} with some [text] that looks like links.\n")
47
48    # Add reference definitions at the end
49    for i in range(num_links):
50        if i % 3 == 1:
51            lines.append(f"[ref{i}]: https://example.com/ref/{i}\n")
52
53    return "".join(lines)
54
55if __name__ == "__main__":
56    profile_link_checker()

執行方式:

1# Direct execution
2python profile_link_checker.py
3
4# Using cProfile from command line
5python -m cProfile -s cumulative markdown_link_checker.py --dir ./docs/

步驟 2:使用 pstats 分析結果

pstats 模組提供更細緻的結果分析功能:

 1import cProfile
 2import pstats
 3from pstats import SortKey
 4
 5def detailed_analysis():
 6    """Perform detailed analysis with pstats"""
 7
 8    # Profile the code
 9    profiler = cProfile.Profile()
10    profiler.enable()
11
12    # Run the target function
13    checker = MarkdownLinkChecker()
14    content = generate_test_content(500)
15    for _ in range(50):
16        checker.parse_markdown_links(content)
17
18    profiler.disable()
19
20    # Create Stats object
21    stats = pstats.Stats(profiler)
22
23    # Different sorting options
24    print("=" * 70)
25    print("Top 10 by CUMULATIVE time (including sub-calls)")
26    print("=" * 70)
27    stats.sort_stats(SortKey.CUMULATIVE).print_stats(10)
28
29    print("\n" + "=" * 70)
30    print("Top 10 by TOTAL time (excluding sub-calls)")
31    print("=" * 70)
32    stats.sort_stats(SortKey.TIME).print_stats(10)
33
34    print("\n" + "=" * 70)
35    print("Top 10 by CALL count")
36    print("=" * 70)
37    stats.sort_stats(SortKey.CALLS).print_stats(10)
38
39    # Filter by function name
40    print("\n" + "=" * 70)
41    print("Functions containing 'parse' or 'match'")
42    print("=" * 70)
43    stats.sort_stats(SortKey.TIME).print_stats('parse|match', 10)
44
45    # Show callers of a specific function
46    print("\n" + "=" * 70)
47    print("Who calls 'finditer'?")
48    print("=" * 70)
49    stats.print_callers('finditer')
50
51    # Save stats to file for later analysis
52    stats.dump_stats('link_checker.prof')
53
54    return stats
55
56def load_and_compare_profiles():
57    """Load and compare saved profile data"""
58    try:
59        # Load saved profile
60        stats = pstats.Stats('link_checker.prof')
61
62        # Add another profile for comparison
63        stats.add('link_checker_optimized.prof')
64
65        # Print combined stats
66        stats.sort_stats(SortKey.CUMULATIVE).print_stats(20)
67    except FileNotFoundError:
68        print("Profile files not found. Run detailed_analysis() first.")

步驟 3:使用 line_profiler 進行行級分析

line_profiler 可以分析每一行程式碼的執行時間,適合精確定位瓶頸。

首先安裝 line_profiler:

1pip install line_profiler

使用裝飾器標記要分析的函式:

  1# profile_lines.py
  2from line_profiler import profile
  3
  4@profile
  5def parse_markdown_links_profiled(self, content: str):
  6    """
  7    Line-by-line profiled version of parse_markdown_links
  8    """
  9    links = []
 10    lines = content.split('\n')
 11
 12    # Collect reference definitions
 13    reference_defs = {}
 14    for match in self.REFERENCE_DEF_PATTERN.finditer(content):
 15        ref_name = match.group(1).lower()
 16        ref_target = match.group(2).strip()
 17        reference_defs[ref_name] = ref_target
 18
 19    in_code_block = False
 20
 21    for line_num, line in enumerate(lines, start=1):
 22        if line.strip().startswith("```"):
 23            in_code_block = not in_code_block
 24            continue
 25
 26        if in_code_block:
 27            continue
 28
 29        # These regex operations might be slow
 30        for match in self.INLINE_LINK_PATTERN.finditer(line):
 31            links.append({
 32                "text": match.group(1),
 33                "target": match.group(2),
 34                "line": line_num
 35            })
 36
 37        for match in self.REFERENCE_USE_PATTERN.finditer(line):
 38            ref_name = match.group(2).lower()
 39            if ref_name in reference_defs:
 40                links.append({
 41                    "text": match.group(1),
 42                    "target": reference_defs[ref_name],
 43                    "line": line_num
 44                })
 45
 46    return links
 47
 48# Alternative: Manual timing for specific sections
 49import time
 50
 51def parse_with_timing(self, content: str):
 52    """Manual timing for detailed analysis"""
 53    timings = {}
 54
 55    # Time: split lines
 56    start = time.perf_counter()
 57    lines = content.split('\n')
 58    timings['split_lines'] = time.perf_counter() - start
 59
 60    # Time: parse reference definitions
 61    start = time.perf_counter()
 62    reference_defs = {}
 63    for match in self.REFERENCE_DEF_PATTERN.finditer(content):
 64        ref_name = match.group(1).lower()
 65        ref_target = match.group(2).strip()
 66        reference_defs[ref_name] = ref_target
 67    timings['parse_refs'] = time.perf_counter() - start
 68
 69    # Time: main parsing loop
 70    start = time.perf_counter()
 71    links = []
 72    in_code_block = False
 73
 74    for line_num, line in enumerate(lines, start=1):
 75        if line.strip().startswith("```"):
 76            in_code_block = not in_code_block
 77            continue
 78
 79        if in_code_block:
 80            continue
 81
 82        for match in self.INLINE_LINK_PATTERN.finditer(line):
 83            links.append({
 84                "text": match.group(1),
 85                "target": match.group(2),
 86                "line": line_num
 87            })
 88
 89        for match in self.REFERENCE_USE_PATTERN.finditer(line):
 90            ref_name = match.group(2).lower()
 91            if ref_name in reference_defs:
 92                links.append({
 93                    "text": match.group(1),
 94                    "target": reference_defs[ref_name],
 95                    "line": line_num
 96                })
 97
 98    timings['main_loop'] = time.perf_counter() - start
 99
100    return links, timings

執行 line_profiler:

1# Run with kernprof
2kernprof -l -v profile_lines.py
3
4# Or use the newer approach
5python -m line_profiler profile_lines.py

步驟 4:分析正則表達式效能

正則表達式是常見的效能瓶頸,需要特別分析:

  1import re
  2import time
  3from typing import Callable
  4
  5def benchmark_regex_patterns():
  6    """Compare different regex pattern implementations"""
  7
  8    # Test content with various edge cases
  9    test_lines = [
 10        "Check out [Link](https://example.com)",
 11        "See [Text with [brackets]](/python-advanced/04-cpython-internals/case-studies/profiling/url)",
 12        "Multiple [link1](/python-advanced/04-cpython-internals/case-studies/profiling/url1) and [link2](/python-advanced/04-cpython-internals/case-studies/profiling/url2)",
 13        "No links here, just plain text",
 14        "Tricky [text](/python-advanced/04-cpython-internals/case-studies/profiling/url) with more [text](/python-advanced/04-cpython-internals/case-studies/profiling/url) links",
 15        "![Image](/python-advanced/04-cpython-internals/case-studies/profiling/image.png) should be ignored",
 16        "[Link with spaces]( url with spaces )",
 17    ] * 1000
 18
 19    test_content = "\n".join(test_lines)
 20
 21    # Pattern 1: Original pattern
 22    pattern1 = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
 23
 24    # Pattern 2: Possessive-like (using atomic group simulation)
 25    # Note: Python re doesn't support possessive quantifiers directly
 26    pattern2 = re.compile(r'(?<!!)\[([^\]]*)\]\(([^)]*)\)')
 27
 28    # Pattern 3: More specific pattern
 29    pattern3 = re.compile(r'(?<!!)\[([^\[\]]+)\]\(([^()]+)\)')
 30
 31    # Pattern 4: Non-capturing groups where possible
 32    pattern4 = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
 33
 34    patterns = {
 35        "original": pattern1,
 36        "non_greedy": pattern2,
 37        "more_specific": pattern3,
 38        "optimized": pattern4,
 39    }
 40
 41    results = {}
 42
 43    for name, pattern in patterns.items():
 44        # Warmup
 45        for _ in range(10):
 46            list(pattern.finditer(test_content))
 47
 48        # Benchmark
 49        start = time.perf_counter()
 50        for _ in range(100):
 51            matches = list(pattern.finditer(test_content))
 52        elapsed = time.perf_counter() - start
 53
 54        results[name] = {
 55            "time": elapsed,
 56            "matches": len(matches),
 57        }
 58
 59    # Print comparison
 60    print("Regex Pattern Performance Comparison")
 61    print("=" * 60)
 62    print(f"{'Pattern':<20} {'Time (s)':<12} {'Matches':<10}")
 63    print("-" * 60)
 64
 65    baseline = results["original"]["time"]
 66    for name, data in results.items():
 67        speedup = baseline / data["time"]
 68        print(f"{name:<20} {data['time']:<12.4f} {data['matches']:<10} ({speedup:.2f}x)")
 69
 70    return results
 71
 72def analyze_regex_backtracking():
 73    """Analyze potential regex backtracking issues"""
 74    import re
 75
 76    # Patterns that might cause backtracking
 77    problematic_patterns = [
 78        (r'\[(.+)\]\((.+)\)', "Greedy .+ can backtrack"),
 79        (r'\[([^\]]+)\]\(([^)]+)\)', "Negated class - better"),
 80        (r'\[(.*?)\]\((.*?)\)', "Non-greedy - still may backtrack"),
 81    ]
 82
 83    # Pathological input that triggers backtracking
 84    pathological = "[" + "a" * 100 + "]"  # No closing bracket pattern
 85
 86    print("Regex Backtracking Analysis")
 87    print("=" * 60)
 88
 89    for pattern_str, description in problematic_patterns:
 90        pattern = re.compile(pattern_str)
 91
 92        start = time.perf_counter()
 93        try:
 94            # Set a timeout using signal (Unix only)
 95            result = pattern.search(pathological)
 96            elapsed = time.perf_counter() - start
 97            print(f"Pattern: {pattern_str}")
 98            print(f"  Description: {description}")
 99            print(f"  Time: {elapsed:.4f}s")
100            print(f"  Match: {result}")
101            print()
102        except Exception as e:
103            print(f"Pattern: {pattern_str} - Error: {e}")
104
105def compare_compile_vs_inline():
106    """Compare precompiled vs inline regex performance"""
107
108    test_content = generate_test_content(500)
109    iterations = 100
110
111    # Test 1: Precompiled pattern (recommended)
112    compiled_pattern = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
113
114    start = time.perf_counter()
115    for _ in range(iterations):
116        list(compiled_pattern.finditer(test_content))
117    compiled_time = time.perf_counter() - start
118
119    # Test 2: Inline pattern (compiled each time by re module cache)
120    start = time.perf_counter()
121    for _ in range(iterations):
122        list(re.finditer(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)', test_content))
123    inline_time = time.perf_counter() - start
124
125    # Test 3: Pattern compiled inside loop (worst case)
126    start = time.perf_counter()
127    for _ in range(iterations):
128        pattern = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
129        list(pattern.finditer(test_content))
130    loop_compile_time = time.perf_counter() - start
131
132    print("Compile Strategy Comparison")
133    print("=" * 60)
134    print(f"{'Strategy':<25} {'Time (s)':<12} {'Relative':<10}")
135    print("-" * 60)
136    print(f"{'Precompiled (class)':<25} {compiled_time:<12.4f} {'1.00x':<10}")
137    print(f"{'Inline (re cache)':<25} {inline_time:<12.4f} {inline_time/compiled_time:.2f}x")
138    print(f"{'Compile in loop':<25} {loop_compile_time:<12.4f} {loop_compile_time/compiled_time:.2f}x")

步驟 5:優化建議與驗證

根據分析結果,實作優化版本並驗證效果:

  1import re
  2from typing import List, Dict, Tuple
  3from dataclasses import dataclass
  4
  5@dataclass(slots=True)  # Python 3.10+ optimization
  6class LinkInfo:
  7    """Link information with memory-efficient storage"""
  8    text: str
  9    target: str
 10    line: int
 11
 12class OptimizedLinkChecker:
 13    """Optimized Markdown link checker based on profiling results"""
 14
 15    # Precompiled patterns at class level
 16    INLINE_LINK_PATTERN = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
 17    REFERENCE_DEF_PATTERN = re.compile(r'^\s*\[([^\]]+)\]:\s*(.+)$', re.MULTILINE)
 18    REFERENCE_USE_PATTERN = re.compile(r'\[([^\]]+)\]\[([^\]]+)\]')
 19    CODE_BLOCK_PATTERN = re.compile(r'^```')
 20
 21    def parse_markdown_links_optimized(self, content: str) -> List[LinkInfo]:
 22        """
 23        Optimized link parsing with reduced memory allocation
 24
 25        Optimizations:
 26        1. Use dataclass with slots for link storage
 27        2. Single pass where possible
 28        3. Avoid repeated string operations
 29        4. Use local variables for frequently accessed attributes
 30        """
 31        links = []
 32
 33        # Cache pattern references for faster access
 34        inline_pattern = self.INLINE_LINK_PATTERN
 35        ref_use_pattern = self.REFERENCE_USE_PATTERN
 36        ref_def_pattern = self.REFERENCE_DEF_PATTERN
 37
 38        # Collect reference definitions first (single pass over content)
 39        reference_defs = {
 40            match.group(1).lower(): match.group(2).strip()
 41            for match in ref_def_pattern.finditer(content)
 42        }
 43
 44        # Process line by line
 45        in_code_block = False
 46        line_start = 0
 47
 48        for line_num, line_end in enumerate(
 49            self._find_line_positions(content), start=1
 50        ):
 51            line = content[line_start:line_end]
 52
 53            # Fast code block check
 54            if line.lstrip().startswith("```"):
 55                in_code_block = not in_code_block
 56                line_start = line_end + 1
 57                continue
 58
 59            if not in_code_block:
 60                # Parse inline links
 61                for match in inline_pattern.finditer(line):
 62                    links.append(LinkInfo(
 63                        text=match.group(1),
 64                        target=match.group(2),
 65                        line=line_num
 66                    ))
 67
 68                # Parse reference links
 69                for match in ref_use_pattern.finditer(line):
 70                    ref_name = match.group(2).lower()
 71                    target = reference_defs.get(ref_name)
 72                    if target:
 73                        links.append(LinkInfo(
 74                            text=match.group(1),
 75                            target=target,
 76                            line=line_num
 77                        ))
 78
 79            line_start = line_end + 1
 80
 81        return links
 82
 83    def _find_line_positions(self, content: str):
 84        """Generator that yields line end positions"""
 85        pos = 0
 86        while True:
 87            newline = content.find('\n', pos)
 88            if newline == -1:
 89                yield len(content)
 90                break
 91            yield newline
 92            pos = newline + 1
 93
 94def verify_optimization():
 95    """Verify that optimizations maintain correctness and improve performance"""
 96    import time
 97
 98    # Generate test content
 99    test_content = generate_test_content(1000)
100
101    # Original implementation
102    original = MarkdownLinkChecker()
103
104    # Optimized implementation
105    optimized = OptimizedLinkChecker()
106
107    # Verify correctness
108    original_links = original.parse_markdown_links(test_content)
109    optimized_links = optimized.parse_markdown_links_optimized(test_content)
110
111    # Compare results
112    assert len(original_links) == len(optimized_links), \
113        f"Link count mismatch: {len(original_links)} vs {len(optimized_links)}"
114
115    for orig, opt in zip(original_links, optimized_links):
116        assert orig["text"] == opt.text, f"Text mismatch: {orig['text']} vs {opt.text}"
117        assert orig["target"] == opt.target, f"Target mismatch"
118        assert orig["line"] == opt.line, f"Line mismatch"
119
120    print("Correctness verified!")
121
122    # Benchmark
123    iterations = 100
124
125    # Original
126    start = time.perf_counter()
127    for _ in range(iterations):
128        original.parse_markdown_links(test_content)
129    original_time = time.perf_counter() - start
130
131    # Optimized
132    start = time.perf_counter()
133    for _ in range(iterations):
134        optimized.parse_markdown_links_optimized(test_content)
135    optimized_time = time.perf_counter() - start
136
137    print(f"\nPerformance Comparison ({iterations} iterations)")
138    print("=" * 50)
139    print(f"Original:  {original_time:.4f}s")
140    print(f"Optimized: {optimized_time:.4f}s")
141    print(f"Speedup:   {original_time / optimized_time:.2f}x")

完整程式碼

以下是整合所有分析功能的完整腳本:

  1#!/usr/bin/env python3
  2"""
  3Performance Profiling Script for Markdown Link Checker
  4
  5This script demonstrates various profiling techniques:
  61. cProfile for function-level analysis
  72. pstats for detailed statistics
  83. line_profiler for line-by-line analysis
  94. Regex pattern benchmarking
 105. Optimization verification
 11
 12Usage:
 13    python profiling_demo.py [--full|--quick|--regex|--verify]
 14"""
 15
 16import argparse
 17import cProfile
 18import pstats
 19import re
 20import sys
 21import time
 22from dataclasses import dataclass
 23from io import StringIO
 24from pathlib import Path
 25from pstats import SortKey
 26from typing import Dict, List, Optional
 27
 28# ===== Original Implementation =====
 29
 30class MarkdownLinkChecker:
 31    """Original Markdown link checker for comparison"""
 32
 33    INLINE_LINK_PATTERN = re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)')
 34    REFERENCE_DEF_PATTERN = re.compile(r'^\s*\[([^\]]+)\]:\s*(.+)$', re.MULTILINE)
 35    REFERENCE_USE_PATTERN = re.compile(r'\[([^\]]+)\]\[([^\]]+)\]')
 36
 37    def parse_markdown_links(self, content: str) -> List[Dict]:
 38        links = []
 39        lines = content.split('\n')
 40
 41        reference_defs = {}
 42        for match in self.REFERENCE_DEF_PATTERN.finditer(content):
 43            ref_name = match.group(1).lower()
 44            ref_target = match.group(2).strip()
 45            reference_defs[ref_name] = ref_target
 46
 47        in_code_block = False
 48
 49        for line_num, line in enumerate(lines, start=1):
 50            if line.strip().startswith("```"):
 51                in_code_block = not in_code_block
 52                continue
 53
 54            if in_code_block:
 55                continue
 56
 57            for match in self.INLINE_LINK_PATTERN.finditer(line):
 58                links.append({
 59                    "text": match.group(1),
 60                    "target": match.group(2),
 61                    "line": line_num
 62                })
 63
 64            for match in self.REFERENCE_USE_PATTERN.finditer(line):
 65                ref_name = match.group(2).lower()
 66                if ref_name in reference_defs:
 67                    links.append({
 68                        "text": match.group(1),
 69                        "target": reference_defs[ref_name],
 70                        "line": line_num
 71                    })
 72
 73        return links
 74
 75# ===== Test Data Generation =====
 76
 77def generate_test_content(num_links: int) -> str:
 78    """Generate test Markdown content"""
 79    lines = ["# Test Document\n\n"]
 80
 81    for i in range(num_links):
 82        if i % 3 == 0:
 83            lines.append(f"Check out [Link {i}](https://example.com/{i})\n")
 84        elif i % 3 == 1:
 85            lines.append(f"See [Reference {i}][ref{i}]\n")
 86        else:
 87            lines.append(f"Paragraph {i} with some text.\n")
 88
 89    lines.append("\n")
 90    for i in range(num_links):
 91        if i % 3 == 1:
 92            lines.append(f"[ref{i}]: https://example.com/ref/{i}\n")
 93
 94    return "".join(lines)
 95
 96# ===== Profiling Functions =====
 97
 98def run_cprofile_analysis(iterations: int = 50, num_links: int = 500):
 99    """Run cProfile analysis"""
100    print("Running cProfile Analysis")
101    print("=" * 70)
102
103    content = generate_test_content(num_links)
104    checker = MarkdownLinkChecker()
105
106    profiler = cProfile.Profile()
107    profiler.enable()
108
109    for _ in range(iterations):
110        checker.parse_markdown_links(content)
111
112    profiler.disable()
113
114    stats = pstats.Stats(profiler)
115    stats.sort_stats(SortKey.CUMULATIVE)
116
117    print(f"\nTop 15 functions by cumulative time:")
118    print("-" * 70)
119    stats.print_stats(15)
120
121    return stats
122
123def run_regex_benchmark():
124    """Benchmark different regex patterns"""
125    print("\nRegex Pattern Benchmark")
126    print("=" * 70)
127
128    content = generate_test_content(500)
129
130    patterns = {
131        "Original": re.compile(r'(?<!!)\[([^\]]+)\]\(([^)]+)\)'),
132        "Non-greedy inner": re.compile(r'(?<!!)\[([^\]]*?)\]\(([^)]*?)\)'),
133        "Anchored": re.compile(r'(?<!!)\[([^\[\]]+)\]\(([^()]+)\)'),
134    }
135
136    print(f"{'Pattern':<25} {'Time (ms)':<12} {'Matches':<10}")
137    print("-" * 50)
138
139    for name, pattern in patterns.items():
140        # Warmup
141        list(pattern.finditer(content))
142
143        # Benchmark
144        start = time.perf_counter()
145        for _ in range(100):
146            matches = list(pattern.finditer(content))
147        elapsed = (time.perf_counter() - start) * 1000
148
149        print(f"{name:<25} {elapsed:<12.2f} {len(matches):<10}")
150
151def run_compile_comparison():
152    """Compare precompiled vs inline regex"""
153    print("\nCompile Strategy Comparison")
154    print("=" * 70)
155
156    content = generate_test_content(500)
157    iterations = 100
158    pattern_str = r'(?<!!)\[([^\]]+)\]\(([^)]+)\)'
159
160    # Precompiled
161    compiled = re.compile(pattern_str)
162    start = time.perf_counter()
163    for _ in range(iterations):
164        list(compiled.finditer(content))
165    compiled_time = time.perf_counter() - start
166
167    # Inline (uses re module cache)
168    start = time.perf_counter()
169    for _ in range(iterations):
170        list(re.finditer(pattern_str, content))
171    inline_time = time.perf_counter() - start
172
173    print(f"{'Strategy':<25} {'Time (ms)':<12} {'Relative':<10}")
174    print("-" * 50)
175    print(f"{'Precompiled':<25} {compiled_time*1000:<12.2f} {'1.00x':<10}")
176    print(f"{'Inline (cached)':<25} {inline_time*1000:<12.2f} {inline_time/compiled_time:.2f}x")
177
178def main():
179    parser = argparse.ArgumentParser(description="Profiling Demo")
180    parser.add_argument('--full', action='store_true', help='Run full analysis')
181    parser.add_argument('--quick', action='store_true', help='Run quick analysis')
182    parser.add_argument('--regex', action='store_true', help='Run regex benchmark')
183    args = parser.parse_args()
184
185    if args.regex:
186        run_regex_benchmark()
187        run_compile_comparison()
188    elif args.quick:
189        run_cprofile_analysis(iterations=10, num_links=100)
190    else:
191        run_cprofile_analysis()
192        run_regex_benchmark()
193        run_compile_comparison()
194
195if __name__ == "__main__":
196    main()

分析結果範例

執行 cProfile 分析後的典型輸出:

 1Running cProfile Analysis
 2======================================================================
 3
 4Top 15 functions by cumulative time:
 5----------------------------------------------------------------------
 6         125003 function calls in 0.892 seconds
 7
 8   Ordered by: cumulative time
 9
10   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
11       50    0.012    0.000    0.892    0.018 checker.py:45(parse_markdown_links)
12    25000    0.234    0.000    0.567    0.000 {method 'finditer' of 're.Pattern'}
13       50    0.089    0.002    0.089    0.002 {method 'split' of 'str' objects}
14    50000    0.156    0.000    0.156    0.000 {method 'append' of 'list' objects}
15    25000    0.098    0.000    0.098    0.000 {method 'group' of 're.Match'}
16    12500    0.045    0.000    0.045    0.000 {method 'lower' of 'str' objects}
17    12500    0.034    0.000    0.034    0.000 {method 'strip' of 'str' objects}
18    25000    0.067    0.000    0.067    0.000 {method 'startswith' of 'str' objects}
19       50    0.023    0.000    0.023    0.000 {built-in method builtins.enumerate}

從上述結果可以觀察到:

  1. finditer 佔用最多時間:正則表達式匹配是主要瓶頸
  2. split 操作耗時明顯:每次解析都建立新的行列表
  3. append 呼叫頻繁:大量的字典建立和列表操作

line_profiler 的典型輸出:

 1Timer unit: 1e-06 s
 2
 3Total time: 0.456 s
 4File: checker.py
 5Function: parse_markdown_links at line 45
 6
 7Line #      Hits         Time  Per Hit   % Time  Line Contents
 8==============================================================
 9    45                                           def parse_markdown_links(self, content):
10    46        50       1234.0     24.7      0.3      links = []
11    47        50      89012.0   1780.2     19.5      lines = content.split('\n')
12    48
13    49        50         56.0      1.1      0.0      reference_defs = {}
14    50      2500      45678.0     18.3     10.0      for match in self.REFERENCE_DEF_PATTERN.finditer(content):
15    51      2450       3456.0      1.4      0.8          ref_name = match.group(1).lower()
16    52      2450       2345.0      1.0      0.5          ref_target = match.group(2).strip()
17    53      2450       1234.0      0.5      0.3          reference_defs[ref_name] = ref_target
18    54
19    55        50         34.0      0.7      0.0      in_code_block = False
20    56
21    57     25050      12345.0      0.5      2.7      for line_num, line in enumerate(lines, start=1):
22    58     25000      34567.0      1.4      7.6          if line.strip().startswith("```"):
23    59                                                       in_code_block = not in_code_block
24    60                                                       continue
25    61
26    62     25000       5678.0      0.2      1.2          if in_code_block:
27    63                                                       continue
28    64
29    65     25000     156789.0      6.3     34.4          for match in self.INLINE_LINK_PATTERN.finditer(line):
30    66      8350      23456.0      2.8      5.1              links.append({...})
31    67
32    68     25000      78901.0      3.2     17.3          for match in self.REFERENCE_USE_PATTERN.finditer(line):
33    69      2450       1234.0      0.5      0.3              ref_name = match.group(2).lower()
34    70      2450        567.0      0.2      0.1              if ref_name in reference_defs:
35    71      2450       1234.0      0.5      0.3                  links.append({...})
36    72
37    73        50         23.0      0.5      0.0      return links

關鍵發現:

  • 第 65 行(行內連結匹配)佔 34.4%:這是最大的瓶頸
  • 第 47 行(split)佔 19.5%:字串分割是第二大消耗
  • 第 68 行(引用連結匹配)佔 17.3%:也是重要的優化目標

設計權衡

面向不分析使用 cProfile使用 line_profiler
開發時間中等較多
分析粒度函式級行級
效能開銷較高
適用場景簡單程式一般優化精確定位
學習成本中等
結果準確度-非常高

什麼時候該做效能分析?

適合分析:

  • 程式明顯變慢
  • 處理大量資料
  • 發布前的效能驗證
  • 正則表達式複雜時
  • 有迴圈處理大量項目

不建議過早優化:

  • 功能還在開發中
  • 使用頻率很低
  • 效能已經足夠
  • 可讀性更重要時

練習

基礎練習:用 cProfile 分析排序函式

 1"""
 2Exercise 1: Profile different sorting approaches
 3"""
 4import cProfile
 5import pstats
 6import random
 7from pstats import SortKey
 8
 9def bubble_sort(arr):
10    """Bubble sort implementation"""
11    n = len(arr)
12    arr = arr.copy()
13    for i in range(n):
14        for j in range(0, n - i - 1):
15            if arr[j] > arr[j + 1]:
16                arr[j], arr[j + 1] = arr[j + 1], arr[j]
17    return arr
18
19def quick_sort(arr):
20    """Quick sort implementation"""
21    if len(arr) <= 1:
22        return arr
23    pivot = arr[len(arr) // 2]
24    left = [x for x in arr if x < pivot]
25    middle = [x for x in arr if x == pivot]
26    right = [x for x in arr if x > pivot]
27    return quick_sort(left) + middle + quick_sort(right)
28
29def profile_sorting():
30    """Profile and compare sorting algorithms"""
31    data = [random.randint(0, 10000) for _ in range(1000)]
32
33    # Profile bubble sort
34    profiler = cProfile.Profile()
35    profiler.enable()
36    bubble_sort(data)
37    profiler.disable()
38
39    print("Bubble Sort Profile:")
40    stats = pstats.Stats(profiler)
41    stats.sort_stats(SortKey.TIME).print_stats(5)
42
43    # Profile quick sort
44    profiler = cProfile.Profile()
45    profiler.enable()
46    quick_sort(data)
47    profiler.disable()
48
49    print("\nQuick Sort Profile:")
50    stats = pstats.Stats(profiler)
51    stats.sort_stats(SortKey.TIME).print_stats(5)
52
53if __name__ == "__main__":
54    profile_sorting()

進階練習:用 line_profiler 找出熱點程式碼

 1"""
 2Exercise 2: Use line_profiler to find hotspots
 3
 4Instructions:
 51. Install line_profiler: pip install line_profiler
 62. Add @profile decorator to functions you want to analyze
 73. Run: kernprof -l -v exercise2.py
 8"""
 9from line_profiler import profile
10
11@profile
12def find_primes(n):
13    """Find all prime numbers up to n"""
14    primes = []
15    for num in range(2, n + 1):
16        is_prime = True
17        for i in range(2, int(num ** 0.5) + 1):
18            if num % i == 0:
19                is_prime = False
20                break
21        if is_prime:
22            primes.append(num)
23    return primes
24
25@profile
26def find_primes_sieve(n):
27    """Find primes using Sieve of Eratosthenes"""
28    sieve = [True] * (n + 1)
29    sieve[0] = sieve[1] = False
30
31    for i in range(2, int(n ** 0.5) + 1):
32        if sieve[i]:
33            for j in range(i * i, n + 1, i):
34                sieve[j] = False
35
36    return [i for i, is_prime in enumerate(sieve) if is_prime]
37
38if __name__ == "__main__":
39    print("Finding primes up to 10000...")
40    primes1 = find_primes(10000)
41    primes2 = find_primes_sieve(10000)
42    print(f"Found {len(primes1)} primes (basic)")
43    print(f"Found {len(primes2)} primes (sieve)")

挑戰題:比較不同正則表達式寫法的效能差異

 1"""
 2Exercise 3: Compare regex pattern performance
 3
 4Task: Parse email addresses from text using different patterns
 5and measure their performance.
 6"""
 7import re
 8import time
 9
10def benchmark_email_patterns():
11    """Compare different email regex patterns"""
12
13    # Test content with mixed valid and invalid emails
14    test_text = """
15    Contact us at support@example.com or sales@company.org
16    Invalid: not.an.email, @missing.com, missing@
17    Valid: user.name+tag@domain.co.uk, test123@sub.domain.com
18    Edge cases: "quoted"@domain.com, user@[192.168.1.1]
19    """ * 1000
20
21    patterns = {
22        # Simple pattern (may miss some valid emails)
23        "simple": re.compile(r'\b[\w.-]+@[\w.-]+\.\w+\b'),
24
25        # More comprehensive pattern
26        "comprehensive": re.compile(
27            r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
28        ),
29
30        # RFC 5322 inspired (complex but thorough)
31        "rfc_inspired": re.compile(
32            r'(?:[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*'
33            r'|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]'
34            r'|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")'
35            r'@(?:(?:[a-z0-9](/python-advanced/04-cpython-internals/case-studies/profiling/?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](/python-advanced/04-cpython-internals/case-studies/profiling/?:[a-z0-9-]*[a-z0-9])?'
36            r'|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
37            r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:'
38            r'(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]'
39            r'|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])',
40            re.IGNORECASE
41        ),
42    }
43
44    print("Email Pattern Performance Comparison")
45    print("=" * 60)
46    print(f"{'Pattern':<20} {'Time (ms)':<12} {'Matches':<10}")
47    print("-" * 60)
48
49    for name, pattern in patterns.items():
50        # Warmup
51        list(pattern.findall(test_text))
52
53        # Benchmark
54        start = time.perf_counter()
55        for _ in range(100):
56            matches = pattern.findall(test_text)
57        elapsed = (time.perf_counter() - start) * 1000
58
59        print(f"{name:<20} {elapsed:<12.2f} {len(matches):<10}")
60
61    # Your task: Add more patterns and analyze the results
62    # Consider: What trade-offs exist between accuracy and speed?
63
64if __name__ == "__main__":
65    benchmark_email_patterns()

延伸閱讀


下一章:記憶體優化