hpc-lab-code/work/analyze_mpi_openmp.py
2026-01-22 04:31:52 +08:00

584 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
MPI+OpenMP混合并行矩阵乘法性能实验数据分析脚本
包含三个实验的完整分析和可视化
"""
import matplotlib.pyplot as plt
import numpy as np
import matplotlib
from matplotlib import rcParams
import pandas as pd
# 设置字体
matplotlib.rcParams['font.sans-serif'] = ['DejaVu Sans']
matplotlib.rcParams['axes.unicode_minus'] = False
# 读取实验数据
def load_data():
"""加载CSV格式的实验数据"""
df = pd.read_csv('experiment_results.csv')
serial_df = pd.read_csv('serial_results.csv')
return df, serial_df
def experiment1_analysis(df, serial_df):
"""实验一固定OpenMP线程数为1改变MPI进程数"""
print("=" * 100)
print("实验一OpenMP线程数=1改变MPI进程数对性能的影响")
print("=" * 100)
# 筛选实验一数据OpenMP线程数=1
exp1_data = df[(df['Experiment'] == 'Exp1') & (df['OpenMP_Threads'] == 1)].copy()
matrix_sizes = [512, 1024, 2048, 4096]
mpi_processes = [1, 2, 3, 6, 9, 12]
# 打印数据表格
for size in matrix_sizes:
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
print(f"\n矩阵规模: {size}x{size}x{size}")
print("-" * 90)
print(f"{'MPI进程数':<12} {'时间(ms)':<15} {'加速比':<15} {'效率':<15}")
print("-" * 90)
for _, row in size_data.iterrows():
print(f"{int(row['MPI_Processes']):<12} {row['Time_ms']:<15.3f} "
f"{row['Speedup']:<15.4f} {row['Efficiency']:<15.4f}")
# 绘制图表
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
markers = ['o', 's', '^', 'd']
# Figure 1: Execution Time Comparison
ax1 = axes[0, 0]
for i, size in enumerate(matrix_sizes):
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
ax1.plot(size_data['MPI_Processes'], size_data['Time_ms'],
marker=markers[i], linewidth=2, label=f'{size}x{size}', color=colors[i])
ax1.set_xlabel('Number of MPI Processes')
ax1.set_ylabel('Execution Time (ms)')
ax1.set_title('Experiment 1: Execution Time vs MPI Processes')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Figure 2: Speedup Comparison
ax2 = axes[0, 1]
for i, size in enumerate(matrix_sizes):
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
ax2.plot(size_data['MPI_Processes'], size_data['Speedup'],
marker=markers[i], linewidth=2, label=f'{size}x{size}', color=colors[i])
# Add ideal speedup reference line
ax2.plot(size_data['MPI_Processes'], size_data['MPI_Processes'],
'--', linewidth=1, color=colors[i], alpha=0.5)
ax2.set_xlabel('Number of MPI Processes')
ax2.set_ylabel('Speedup')
ax2.set_title('Experiment 1: Speedup vs MPI Processes')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Figure 3: Parallel Efficiency Comparison
ax3 = axes[1, 0]
for i, size in enumerate(matrix_sizes):
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
ax3.plot(size_data['MPI_Processes'], size_data['Efficiency'],
marker=markers[i], linewidth=2, label=f'{size}x{size}', color=colors[i])
# Add ideal efficiency reference line (100%)
ax3.axhline(y=1.0, color='gray', linestyle='--', linewidth=1, alpha=0.5)
ax3.set_xlabel('Number of MPI Processes')
ax3.set_ylabel('Parallel Efficiency')
ax3.set_title('Experiment 1: Parallel Efficiency vs MPI Processes')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Figure 4: Efficiency Heatmap
ax4 = axes[1, 1]
efficiency_matrix = []
for size in matrix_sizes:
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
efficiency_matrix.append(size_data['Efficiency'].values)
im = ax4.imshow(efficiency_matrix, cmap='RdYlGn', aspect='auto', vmin=0, vmax=1)
ax4.set_xticks(range(len(mpi_processes)))
ax4.set_xticklabels(mpi_processes)
ax4.set_yticks(range(len(matrix_sizes)))
ax4.set_yticklabels([f'{s}x{s}' for s in matrix_sizes])
ax4.set_xlabel('Number of MPI Processes')
ax4.set_ylabel('Matrix Size')
ax4.set_title('Parallel Efficiency Heatmap')
# Add value annotations
for i in range(len(matrix_sizes)):
for j in range(len(mpi_processes)):
text = ax4.text(j, i, f'{efficiency_matrix[i][j]:.2f}',
ha="center", va="center", color="black", fontsize=8)
plt.colorbar(im, ax=ax4, label='Efficiency')
plt.tight_layout()
plt.savefig('experiment1_analysis.png', dpi=300, bbox_inches='tight')
print("\nFigure saved to: experiment1_analysis.png")
return exp1_data
def experiment2_analysis(df):
"""实验二同时改变MPI进程数和OpenMP线程数"""
print("\n" + "=" * 100)
print("实验二MPI进程数和OpenMP线程数同时改变对性能的影响")
print("=" * 100)
# 筛选实验二数据
exp2_data = df[df['Experiment'] == 'Exp2'].copy()
matrix_sizes = [512, 1024, 2048, 4096]
mpi_processes = [1, 2, 3, 6, 9, 12]
omp_threads = [1, 2, 4, 8]
# 2.1 打印总体数据表格
print("\n2.1 不同配置下的性能数据")
for size in matrix_sizes:
print(f"\n矩阵规模: {size}x{size}x{size}")
print("-" * 100)
print(f"{'MPI':<6} {'OMP':<6} {'总进程数':<10} {'时间(ms)':<15} {'加速比':<15} {'效率':<15}")
print("-" * 100)
size_data = exp2_data[exp2_data['M'] == size]
for np in mpi_processes:
for nt in omp_threads:
row = size_data[(size_data['MPI_Processes'] == np) &
(size_data['OpenMP_Threads'] == nt)]
if not row.empty:
r = row.iloc[0]
total_procs = r['MPI_Processes'] * r['OpenMP_Threads']
print(f"{int(r['MPI_Processes']):<6} {int(r['OpenMP_Threads']):<6} "
f"{int(total_procs):<10} {r['Time_ms']:<15.3f} "
f"{r['Speedup']:<15.4f} {r['Efficiency']:<15.4f}")
# 2.2 分析相同总进程数下不同分配的影响
print("\n\n2.2 相同总进程数下MPI进程数和OpenMP线程数分配对效率的影响")
print("=" * 100)
# 找出总进程数相同的配置组合
combinations = [
(1, 16), (2, 8), (4, 4), (8, 2), (16, 1) # 总进程数=16
]
for size in [512, 1024, 2048, 4096]:
print(f"\n矩阵规模: {size}x{size}x{size},总进程数=16的不同分配")
print("-" * 90)
print(f"{'MPI进程数':<12} {'OpenMP线程数':<15} {'时间(ms)':<15} {'加速比':<15} {'效率':<15}")
print("-" * 90)
size_data = exp2_data[exp2_data['M'] == size]
for np, nt in combinations:
row = size_data[(size_data['MPI_Processes'] == np) &
(size_data['OpenMP_Threads'] == nt)]
if not row.empty:
r = row.iloc[0]
print(f"{int(r['MPI_Processes']):<12} {int(r['OpenMP_Threads']):<15} "
f"{r['Time_ms']:<15.3f} {r['Speedup']:<15.4f} {r['Efficiency']:<15.4f}")
# 找出最优配置
best_config = None
best_efficiency = 0
for np, nt in combinations:
row = size_data[(size_data['MPI_Processes'] == np) &
(size_data['OpenMP_Threads'] == nt)]
if not row.empty:
eff = row.iloc[0]['Efficiency']
if eff > best_efficiency:
best_efficiency = eff
best_config = (np, nt)
if best_config:
print(f"\n最优配置: MPI={best_config[0]}, OpenMP={best_config[1]}, "
f"效率={best_efficiency:.4f}")
# 绘制图表
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# Figure 1: Efficiency comparison for total processes = 16
ax1 = axes[0, 0]
size = 1024 # Use 1024 as example
size_data = exp2_data[exp2_data['M'] == size]
configs = []
efficiencies = []
for np, nt in combinations:
row = size_data[(size_data['MPI_Processes'] == np) &
(size_data['OpenMP_Threads'] == nt)]
if not row.empty:
configs.append(f'{np}x{nt}')
efficiencies.append(row.iloc[0]['Efficiency'])
bars = ax1.bar(range(len(configs)), efficiencies, color='steelblue', alpha=0.7)
ax1.set_xticks(range(len(configs)))
ax1.set_xticklabels([f'MPI={c.split("x")[0]}\nOMP={c.split("x")[1]}' for c in configs])
ax1.set_ylabel('Parallel Efficiency')
ax1.set_title(f'Efficiency Comparison (Total Processes=16, {size}x{size})')
ax1.axhline(y=1.0, color='red', linestyle='--', linewidth=1, alpha=0.5, label='Ideal')
ax1.legend()
ax1.grid(True, alpha=0.3, axis='y')
# Add value annotations
for i, (bar, eff) in enumerate(zip(bars, efficiencies)):
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{eff:.3f}', ha='center', va='bottom', fontsize=9)
# Figure 2: Best configuration efficiency for different matrix sizes
ax2 = axes[0, 1]
matrix_sizes_for_plot = [512, 1024, 2048, 4096]
best_efficiencies = []
best_configs_labels = []
for size in matrix_sizes_for_plot:
size_data = exp2_data[exp2_data['M'] == size]
best_eff = 0
best_config = None
for np, nt in combinations:
row = size_data[(size_data['MPI_Processes'] == np) &
(size_data['OpenMP_Threads'] == nt)]
if not row.empty:
eff = row.iloc[0]['Efficiency']
if eff > best_eff:
best_eff = eff
best_config = f'{np}x{nt}'
best_efficiencies.append(best_eff)
best_configs_labels.append(best_config)
bars = ax2.bar(range(len(matrix_sizes_for_plot)), best_efficiencies,
color='coral', alpha=0.7)
ax2.set_xticks(range(len(matrix_sizes_for_plot)))
ax2.set_xticklabels([f'{s}x{s}' for s in matrix_sizes_for_plot])
ax2.set_ylabel('Best Parallel Efficiency')
ax2.set_title('Best Configuration Efficiency vs Matrix Size')
ax2.axhline(y=1.0, color='red', linestyle='--', linewidth=1, alpha=0.5, label='Ideal')
ax2.legend()
ax2.grid(True, alpha=0.3, axis='y')
# Add configuration annotations
for i, (bar, eff, config) in enumerate(zip(bars, best_efficiencies, best_configs_labels)):
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{eff:.3f}\n{config}', ha='center', va='bottom', fontsize=8)
# Figure 3: Impact of MPI processes on efficiency (fixed OpenMP threads)
ax3 = axes[1, 0]
for nt in [1, 2, 4, 8]:
efficiencies_by_size = {}
for size in matrix_sizes_for_plot:
size_data = exp2_data[(exp2_data['M'] == size) & (exp2_data['OpenMP_Threads'] == nt)]
if not size_data.empty:
# Calculate average efficiency
avg_eff = size_data['Efficiency'].mean()
efficiencies_by_size[size] = avg_eff
if efficiencies_by_size:
ax3.plot(efficiencies_by_size.keys(), efficiencies_by_size.values(),
marker='o', linewidth=2, label=f'OpenMP={nt}')
ax3.set_xlabel('Matrix Size')
ax3.set_ylabel('Average Parallel Efficiency')
ax3.set_title('MPI Process Impact on Efficiency (Fixed OpenMP Threads)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# Figure 4: Speedup comparison (different configurations)
ax4 = axes[1, 1]
for size in [512, 2048]:
size_data = exp2_data[exp2_data['M'] == size]
for nt in [1, 2, 4, 8]:
nt_data = size_data[size_data['OpenMP_Threads'] == nt].sort_values('MPI_Processes')
if not nt_data.empty:
total_procs = nt_data['MPI_Processes'] * nt_data['OpenMP_Threads']
ax4.plot(total_procs, nt_data['Speedup'],
marker='o', linewidth=2,
label=f'{size}x{size}, OMP={nt}')
# Add ideal speedup reference line
max_procs = 96
ax4.plot(range(1, max_procs+1), range(1, max_procs+1),
'--', linewidth=1, color='gray', alpha=0.5, label='Ideal')
ax4.set_xlabel('Total Processes (MPI × OpenMP)')
ax4.set_ylabel('Speedup')
ax4.set_title('Speedup Comparison for Different Configurations')
ax4.legend(fontsize=8)
ax4.grid(True, alpha=0.3)
ax4.set_xlim(0, max_procs)
ax4.set_ylim(0, max_procs)
plt.tight_layout()
plt.savefig('experiment2_analysis.png', dpi=300, bbox_inches='tight')
print("\nFigure saved to: experiment2_analysis.png")
return exp2_data
def experiment3_analysis(df):
"""实验三:优化前后的性能对比"""
print("\n" + "=" * 100)
print("实验三:优化前后的性能对比分析")
print("=" * 100)
# 筛选实验三数据
exp3_original = df[df['Experiment'] == 'Exp3'].copy()
exp3_optimized = df[df['Experiment'] == 'Exp3-opt'].copy()
matrix_sizes = [512, 1024, 2048, 4096]
combinations = [(1, 16), (2, 8), (4, 4), (8, 2), (16, 1)]
# 打印优化前后对比表格
for size in matrix_sizes:
print(f"\n矩阵规模: {size}x{size}x{size}")
print("-" * 110)
print(f"{'配置':<15} {'优化前时间(ms)':<18} {'优化后时间(ms)':<18} "
f"{'性能提升':<15} {'优化前效率':<15} {'优化后效率':<15}")
print("-" * 110)
for np, nt in combinations:
orig_row = exp3_original[(exp3_original['M'] == size) &
(exp3_original['MPI_Processes'] == np) &
(exp3_original['OpenMP_Threads'] == nt)]
opt_row = exp3_optimized[(exp3_optimized['M'] == size) &
(exp3_optimized['MPI_Processes'] == np) &
(exp3_optimized['OpenMP_Threads'] == nt)]
if not orig_row.empty and not opt_row.empty:
orig = orig_row.iloc[0]
opt = opt_row.iloc[0]
speedup = orig['Time_ms'] / opt['Time_ms']
print(f"{np}×{nt:<10} {orig['Time_ms']:<18.3f} {opt['Time_ms']:<18.3f} "
f"{speedup:<15.2f}x {orig['Efficiency']:<15.4f} {opt['Efficiency']:<15.4f}")
# 绘制图表
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# Figure 1: Execution time comparison before and after optimization
ax1 = axes[0, 0]
size = 1024
configs = []
orig_times = []
opt_times = []
for np, nt in combinations:
orig_row = exp3_original[(exp3_original['M'] == size) &
(exp3_original['MPI_Processes'] == np) &
(exp3_original['OpenMP_Threads'] == nt)]
opt_row = exp3_optimized[(exp3_optimized['M'] == size) &
(exp3_optimized['MPI_Processes'] == np) &
(exp3_optimized['OpenMP_Threads'] == nt)]
if not orig_row.empty and not opt_row.empty:
configs.append(f'{np}x{nt}')
orig_times.append(orig_row.iloc[0]['Time_ms'])
opt_times.append(opt_row.iloc[0]['Time_ms'])
x = list(range(len(configs)))
width = 0.35
ax1.bar([i - width/2 for i in x], orig_times, width, label='Original', color='coral', alpha=0.7)
ax1.bar([i + width/2 for i in x], opt_times, width, label='Optimized', color='steelblue', alpha=0.7)
ax1.set_xticks(x)
ax1.set_xticklabels(configs)
ax1.set_ylabel('Execution Time (ms)')
ax1.set_title(f'Execution Time Comparison ({size}x{size})')
ax1.legend()
ax1.grid(True, alpha=0.3, axis='y')
# Figure 2: Efficiency comparison before and after optimization
ax2 = axes[0, 1]
orig_effs = []
opt_effs = []
for np, nt in combinations:
orig_row = exp3_original[(exp3_original['M'] == size) &
(exp3_original['MPI_Processes'] == np) &
(exp3_original['OpenMP_Threads'] == nt)]
opt_row = exp3_optimized[(exp3_optimized['M'] == size) &
(exp3_optimized['MPI_Processes'] == np) &
(exp3_optimized['OpenMP_Threads'] == nt)]
if not orig_row.empty and not opt_row.empty:
orig_effs.append(orig_row.iloc[0]['Efficiency'])
opt_effs.append(opt_row.iloc[0]['Efficiency'])
x = list(range(len(configs)))
ax2.plot(x, orig_effs, marker='o', linewidth=2, label='Original', color='coral')
ax2.plot(x, opt_effs, marker='s', linewidth=2, label='Optimized', color='steelblue')
ax2.set_xticks(x)
ax2.set_xticklabels(configs)
ax2.set_ylabel('Parallel Efficiency')
ax2.set_title(f'Efficiency Comparison ({size}x{size})')
ax2.axhline(y=1.0, color='red', linestyle='--', linewidth=1, alpha=0.5, label='Ideal')
ax2.legend()
ax2.grid(True, alpha=0.3)
# Figure 3: Performance improvement for different matrix sizes
ax3 = axes[1, 0]
matrix_sizes_for_plot = [512, 1024, 2048, 4096]
speedups_by_config = {config: [] for config in combinations}
for size in matrix_sizes_for_plot:
for np, nt in combinations:
orig_row = exp3_original[(exp3_original['M'] == size) &
(exp3_original['MPI_Processes'] == np) &
(exp3_original['OpenMP_Threads'] == nt)]
opt_row = exp3_optimized[(exp3_optimized['M'] == size) &
(exp3_optimized['MPI_Processes'] == np) &
(exp3_optimized['OpenMP_Threads'] == nt)]
if not orig_row.empty and not opt_row.empty:
speedup = orig_row.iloc[0]['Time_ms'] / opt_row.iloc[0]['Time_ms']
speedups_by_config[(np, nt)].append(speedup)
for i, (np, nt) in enumerate(combinations):
if speedups_by_config[(np, nt)]:
ax3.plot(matrix_sizes_for_plot, speedups_by_config[(np, nt)],
marker='o', linewidth=2, label=f'{np}x{nt}')
ax3.set_xlabel('Matrix Size')
ax3.set_ylabel('Performance Improvement (x)')
ax3.set_title('Optimization Effect for Different Matrix Sizes')
ax3.axhline(y=1.0, color='gray', linestyle='--', linewidth=1, alpha=0.5)
ax3.legend()
ax3.grid(True, alpha=0.3)
# Figure 4: Best configuration efficiency comparison
ax4 = axes[1, 1]
best_orig_effs = []
best_opt_effs = []
for size in matrix_sizes_for_plot:
# Find best configuration
best_orig_eff = 0
best_opt_eff = 0
for np, nt in combinations:
orig_row = exp3_original[(exp3_original['M'] == size) &
(exp3_original['MPI_Processes'] == np) &
(exp3_original['OpenMP_Threads'] == nt)]
opt_row = exp3_optimized[(exp3_optimized['M'] == size) &
(exp3_optimized['MPI_Processes'] == np) &
(exp3_optimized['OpenMP_Threads'] == nt)]
if not orig_row.empty:
best_orig_eff = max(best_orig_eff, orig_row.iloc[0]['Efficiency'])
if not opt_row.empty:
best_opt_eff = max(best_opt_eff, opt_row.iloc[0]['Efficiency'])
best_orig_effs.append(best_orig_eff)
best_opt_effs.append(best_opt_eff)
x = list(range(len(matrix_sizes_for_plot)))
width = 0.35
ax4.bar([i - width/2 for i in x], best_orig_effs, width, label='Original', color='coral', alpha=0.7)
ax4.bar([i + width/2 for i in x], best_opt_effs, width, label='Optimized', color='steelblue', alpha=0.7)
ax4.set_xticks(x)
ax4.set_xticklabels([f'{s}x{s}' for s in matrix_sizes_for_plot])
ax4.set_ylabel('Best Parallel Efficiency')
ax4.set_title('Best Configuration Efficiency Comparison')
ax4.axhline(y=1.0, color='red', linestyle='--', linewidth=1, alpha=0.5, label='Ideal')
ax4.legend()
ax4.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig('experiment3_analysis.png', dpi=300, bbox_inches='tight')
print("\nFigure saved to: experiment3_analysis.png")
return exp3_original, exp3_optimized
def analyze_bottlenecks(df):
"""分析性能瓶颈"""
print("\n" + "=" * 100)
print("性能瓶颈分析")
print("=" * 100)
exp1_data = df[df['Experiment'] == 'Exp1']
exp2_data = df[df['Experiment'] == 'Exp2']
print("\n1. MPI扩展性分析")
print("-" * 90)
# 分析MPI进程数增加时的效率下降
for size in [512, 1024, 2048, 4096]:
size_data = exp1_data[exp1_data['M'] == size].sort_values('MPI_Processes')
if not size_data.empty:
print(f"\n矩阵规模 {size}x{size}:")
for _, row in size_data.iterrows():
np = row['MPI_Processes']
eff = row['Efficiency']
if np == 1:
print(f" {np}进程: 效率={eff:.4f} (基准)")
else:
prev_data = size_data[size_data['MPI_Processes'] == np/2] if np % 2 == 1 else size_data[size_data['MPI_Processes'] == np-1]
if not prev_data.empty and np > 1:
prev_eff = prev_data.iloc[0]['Efficiency']
eff_change = (eff - prev_eff) / prev_eff * 100
print(f" {np}进程: 效率={eff:.4f} (变化: {eff_change:+.1f}%)")
print("\n\n2. OpenMP线程数扩展性分析")
print("-" * 90)
# 分析OpenMP线程数增加时的效率
for size in [512, 1024, 2048, 4096]:
print(f"\n矩阵规模 {size}x{size}:")
size_data = exp2_data[exp2_data['M'] == size]
for np in [1, 2, 3]:
np_data = size_data[size_data['MPI_Processes'] == np]
if not np_data.empty:
print(f" MPI进程数={np}:")
for _, row in np_data.sort_values('OpenMP_Threads').iterrows():
nt = row['OpenMP_Threads']
eff = row['Efficiency']
print(f" OpenMP线程数={nt}: 效率={eff:.4f}")
print("\n\n3. 通信开销分析")
print("-" * 90)
print("MPI进程数增加时通信开销增大导致效率下降")
print(" - 进程间通信需要同步和等待")
print(" - 数据分发和结果收集的开销")
print(" - 负载不均衡导致的空闲等待")
print("\n\n4. 内存带宽瓶颈")
print("-" * 90)
print("矩阵规模较小时,内存带宽成为瓶颈:")
print(" - 计算时间短,通信时间占比高")
print(" - 缓存利用率低")
print(" - 内存访问模式不优化")
print("\n\n5. 负载均衡问题")
print("-" * 90)
print("MPI进程数不能整除矩阵大小时")
print(" - 部分进程负载较重")
print(" - 进程间等待时间增加")
print(" - 整体效率下降")
def main():
"""主函数"""
print("开始分析MPI+OpenMP混合并行矩阵乘法实验数据...\n")
# 加载数据
df, serial_df = load_data()
# 实验一分析
exp1_data = experiment1_analysis(df, serial_df)
# 实验二分析
exp2_data = experiment2_analysis(df)
# 实验三分析
exp3_orig, exp3_opt = experiment3_analysis(df)
# 瓶颈分析
analyze_bottlenecks(df)
print("\n" + "=" * 100)
print("分析完成!所有图表已保存。")
print("=" * 100)
if __name__ == "__main__":
main()