课程0基础Agent开发课 / Python基础 / Python性能优化-从profiling到numpy向量化
— 30 min read

Python性能优化-从profiling到numpy向量化

Java 开发者第一次跑 Python 批处理任务,往往会被它的速度惊到——不是惊叹快,而是惊叹慢。处理 10 万条 Embedding 请求,Python 循环版本需要几分钟,而 numpy 向量化版本只需要几秒。这不是语言本身的天花板,而是使用方式的差异。

Python 性能优化:从 profiling 到 numpy 向量化

Java 开发者第一次跑 Python 批处理任务,往往会被它的速度惊到——不是惊叹快,而是惊叹慢。处理 10 万条 Embedding 请求,Python 循环版本需要几分钟,而 numpy 向量化版本只需要几秒。这不是语言本身的天花板,而是使用方式的差异。

本章系统介绍 Python 性能优化的完整方法论:先用 profiling 工具找到真正的瓶颈,再用 numpy 向量化、批处理等技术针对性优化。


1.1 第一原则:先 profile,不要猜

Profiling
定位瓶颈
cProfile / line_profiler / py-spy
先测量,不要猜

算法优化
数据结构
时间复杂度
减少循环
最大收益往往在这

NumPy向量化
np.vectorize
矩阵运算
广播机制
AI/ML首选方案

并行化
multiprocessing
asyncio
ConcurrentFutures
充分利用多核CPU

原则:先确认瓶颈再优化,过早优化是万恶之源

从 profiling 定位到并行化的四步性能优化路径

性能优化最常见的错误是凭感觉优化。程序员通常会猜错瓶颈所在,花大量时间优化一个只占总运行时间 2% 的函数。

Knuth 定律:过早优化是万恶之源。先让代码跑起来,再 profile,再优化。

Java 开发者熟悉 JProfiler、VisualVM 等工具。Python 有一套轻量级的命令行 profiling 工具链,无需额外安装即可使用。


1.2 性能优化工作流程

发现性能问题
程序太慢 / 内存太高

cProfile
找函数级瓶颈

找到热点函数?

line_profiler
找行级瓶颈

检查外部依赖
DB / API / 文件 IO

是否是循环?

numpy 向量化
替代 for 循环

算法优化
缓存 / 减少重复计算

timeit
验证优化效果

满足性能目标?

内存分析
tracemalloc

优化完成

连接池 / 批量请求
异步并发


1.3 cProfile:找函数级瓶颈

cProfile 是 Python 标准库自带的性能分析器(profiler,性能剖析工具,记录每个函数的调用次数和耗时,帮你找出程序的瓶颈)。它统计每个函数被调用了多少次,以及累计花费了多少时间。

类比:就像 Java 的 JProfiler 的"方法调用热点"视图,只是命令行输出。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import cProfile
import pstats

def preprocess_text(text: str) -> str:
    """模拟文本预处理"""
    import re
    text = text.lower()
    text = re.sub(r"[^\w\s]", "", text)
    text = " ".join(text.split())
    return text

def process_documents(docs: list[str]) -> list[str]:
    """批量处理文档"""
    return [preprocess_text(doc) for doc in docs]

def main():
    # 模拟 10000 个文档
    docs = ["Hello, World! This is a test document." * 10] * 10_000
    result = process_documents(docs)
    return result

# 启动 profiling
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()

# 打印报告(按累计时间排序,只看前 15 行)
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative")
stats.print_stats(15)

输出示例:

code
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    2.341    2.341 script.py:14(main)
        1    0.023    0.023    2.340    2.340 script.py:10(process_documents)
    10000    0.187    0.000    2.317    0.000 script.py:5(preprocess_text)
    10000    1.893    0.000    1.893    0.000 {built-in method re.sub}

读取方式

  • ncalls:调用次数
  • tottime:函数自身耗时(不含子函数)
  • cumtime:累计耗时(含子函数)——重点关注这列
  • 上例清楚显示:re.sub 是瓶颈,占了 1.9 秒,应该考虑预编译正则表达式

命令行快速使用(无需修改代码):

bash
# 直接对脚本 profiling
python -m cProfile -s cumulative your_script.py | head -30

1.4 line_profiler:找行级瓶颈

cProfile 告诉你哪个函数慢,line_profiler 告诉你函数里哪一行慢。它需要单独安装:

bash
pip install line_profiler
python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

# 在需要分析的函数上加 @profile 装饰器
# 注意:运行时需要用 kernprof 命令,不是直接 python

@profile  # kernprof 会自动注入这个装饰器
def preprocess_text_v2(texts: list[str]) -> list[str]:
    import re
    result = []
    pattern = re.compile(r"[^\w\s]")    # 预编译正则
    for text in texts:
        text = text.lower()             # 第3行
        text = pattern.sub("", text)    # 第4行
        text = " ".join(text.split())   # 第5行
        result.append(text)             # 第6行
    return result

运行命令:

bash
kernprof -l -v your_script.py

输出示例(每行的时间占比一目了然):

code
Line #    Hits    Time  Per Hit   % Time  Line Contents
     3   10000   45230    4.5      4.2%   text = text.lower()
     4   10000  812341   81.2     75.8%   text = pattern.sub("", text)
     5   10000  213456   21.3     19.9%   text = " ".join(text.split())

这种精度让优化工作有的放矢,不再凭感觉。


1.5 timeit:精确测量小片段的性能

timeit 用于对比两段代码的执行速度,自动运行多次取平均,消除单次测量误差。

类比:Java 中的 JMH(Java Microbenchmark Harness),但使用更简单。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import timeit

# 对比:普通字符串拼接 vs join
setup = """
texts = ["word"] * 1000
"""

# 方案一:+ 号拼接(每次创建新字符串,内存开销大)
stmt1 = """
result = ""
for t in texts:
    result += t + " "
"""

# 方案二:join(只创建一次字符串)
stmt2 = """
result = " ".join(texts)
"""

t1 = timeit.timeit(stmt1, setup=setup, number=1000)
t2 = timeit.timeit(stmt2, setup=setup, number=1000)
print(f"+ 号拼接: {t1:.3f}s")
print(f"join:     {t2:.3f}s")
print(f"join 快了 {t1/t2:.1f} 倍")
# 典型输出:
# + 号拼接: 0.523s
# join:     0.028s
# join 快了 18.7 倍

1.6 Python 循环的性能陷阱

Python 的 for 循环速度让 Java 开发者难以置信地慢。下面是量化对比:

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import time
import numpy as np

N = 1_000_000  # 100 万次运算

# 方式一:Python 原生 for 循环
start = time.perf_counter()
result = 0.0
for i in range(N):
    result += i * 0.5
python_time = time.perf_counter() - start
print(f"Python 循环: {python_time:.3f}s")   # 约 0.08s

# 方式二:numpy 向量化
start = time.perf_counter()
arr = np.arange(N, dtype=float)
result = np.sum(arr * 0.5)
numpy_time = time.perf_counter() - start
print(f"numpy 向量化: {numpy_time:.4f}s")  # 约 0.002s

print(f"numpy 快了 {python_time/numpy_time:.0f} 倍")
# 典型输出:numpy 快了 40 倍

为什么 Python 循环慢
Python 是动态类型语言,每次循环迭代都需要检查变量类型、进行类型推断、调用对象方法。这些在 Java 中由 JIT 编译器(Just-In-Time,即时编译器,将热点代码在运行时直接编译成机器码以提速)消除的开销,Python 解释器每次都要承担。100 万次循环 ≈ 100 万次类型检查。


1.7 numpy 向量化:用数组思维替代循环

numpy(Numerical Python 的缩写)是 Python 科学计算的基础库。它的核心是 ndarray(N 维数组),底层用 C 语言实现,通过向量化操作(一次对整个数组运算)绕过 Python 解释器的逐行开销。

类比:Python 的 for 循环像 Java 的 for (Item item : list) 逐个处理,numpy 向量化像 Java 8 的 list.parallelStream().map(...) 批量并行处理,但更快,因为底层是 C 语言的 SIMD 指令(Single Instruction Multiple Data,一条指令同时对多个数据执行同一操作的 CPU 特性)。

1.7.1 基本向量化模式

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import numpy as np

# 场景:计算 100 万个文本的平均词长
word_counts = np.random.randint(10, 500, size=1_000_000)  # 模拟词数
char_counts = np.random.randint(50, 5000, size=1_000_000) # 模拟字符数

# 不好的写法:Python for 循环
def avg_word_len_slow(words, chars):
    result = []
    for w, c in zip(words, chars):
        if w > 0:
            result.append(c / w)
    return result

# 好的写法:numpy 向量化
def avg_word_len_fast(words, chars):
    mask = words > 0                          # 布尔数组,表示哪些位置满足条件
    result = np.zeros(len(words))
    result[mask] = chars[mask] / words[mask]  # 只对满足条件的位置运算
    return result
操作类型 Python 循环写法 numpy 向量化写法
加法 [x + 1 for x in arr] arr + 1
条件过滤 [x for x in arr if x > 0] arr[arr > 0]
两数组运算 [a*b for a,b in zip(a,b)] a * b
求和 sum(arr) np.sum(arr) 快 10x
矩阵乘法 需三层 for 循环 np.dot(A, B) 快 100x+

1.8 内存优化

1.8.1 tracemalloc:找内存泄漏

内存泄漏(Memory Leak):程序不断申请内存,但不释放,最终 OOM(Out of Memory,内存耗尽,程序被系统强制终止)崩溃。AI 项目尤其容易出现:每个 Embedding 向量都要存在内存里,积累一段时间就会吃光内存。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import tracemalloc

tracemalloc.start()

# 模拟可能有内存问题的代码
embeddings_cache = {}
for i in range(100_000):
    # 如果 key 是动态生成的,缓存会无限增长
    embeddings_cache[f"text_{i}"] = [0.1] * 1536  # 1536 维向量

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

print("内存占用 TOP 10:")
for stat in top_stats[:10]:
    print(stat)

1.8.2 生成器 vs 列表的内存差异

生成器(Generator)是 Python 的一种惰性计算机制:不预先计算所有结果,而是每次被请求时才计算一个。类比 Java 的 Stream(懒加载模式)vs List(全部加载到内存)。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import sys

# 列表推导式:立即计算全部,全部存入内存
list_result = [x ** 2 for x in range(1_000_000)]
print(f"列表内存: {sys.getsizeof(list_result) / 1024 / 1024:.1f} MB")
# 输出:列表内存: 8.1 MB

# 生成器表达式:不存储结果,按需计算
gen_result = (x ** 2 for x in range(1_000_000))
print(f"生成器内存: {sys.getsizeof(gen_result)} bytes")
# 输出:生成器内存: 104 bytes  ← 几乎为零!

# 在只需要遍历一次的场景下,生成器是更好的选择
# 例如:逐批把向量写入数据库,不需要全部同时在内存中
def embed_generator(texts):
    for text in texts:
        yield compute_embedding(text)  # 一次只计算一个,用完即丢
场景 推荐方式 原因
需要多次遍历 列表 生成器只能遍历一次
只遍历一次,数据量大 生成器 节省内存,O(1) 内存占用
需要随机访问(按下标) 列表 生成器不支持 gen[i]
流式处理大文件 生成器 避免全文件载入内存

1.9 AI 项目常见性能瓶颈

1.9.1 文本 Embedding:批处理 vs 逐条处理

Embedding(嵌入)是将文本转换为向量(一组数字,用于表示文本语义)的操作,是 RAG(检索增强生成)系统的核心步骤。每次调用 Embedding API 都有网络往返延迟(RTT,Round-Trip Time,请求从发出到收到响应的总时间),逐条调用是最低效的方式。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

from openai import OpenAI
import time

client = OpenAI()
texts = ["这是一段文字"] * 1000  # 模拟 1000 条文本

# 方式一:逐条调用(1000 次 HTTP 请求)
start = time.perf_counter()
embeddings_slow = []
for text in texts:
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=text  # 每次只发一条
    )
    embeddings_slow.append(resp.data[0].embedding)
slow_time = time.perf_counter() - start
print(f"逐条处理: {slow_time:.1f}s")  # 约 50-100s

# 方式二:批量调用(分批,每批 100 条,共 10 次 HTTP 请求)
BATCH_SIZE = 100
start = time.perf_counter()
embeddings_fast = []
for i in range(0, len(texts), BATCH_SIZE):
    batch = texts[i:i + BATCH_SIZE]
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=batch  # 一次发 100 条
    )
    embeddings_fast.extend([d.embedding for d in resp.data])
fast_time = time.perf_counter() - start
print(f"批量处理: {fast_time:.1f}s")  # 约 3-5s

print(f"批量处理快了约 {slow_time/fast_time:.0f} 倍")

1.9.2 向量相似度:numpy 优化

RAG 检索的核心操作是计算查询向量与知识库向量的余弦相似度(Cosine Similarity,衡量两个向量方向的相似程度,值越接近1表示语义越相近)。用 numpy 矩阵运算比 for 循环快几十倍。

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import numpy as np

# 知识库:10000 个文档,每个向量 1536 维
doc_vectors = np.random.randn(10_000, 1536).astype(np.float32)
query_vector = np.random.randn(1536).astype(np.float32)

# 方式一:Python for 循环计算余弦相似度
def cosine_sim_slow(query, docs):
    results = []
    q_norm = np.linalg.norm(query)
    for doc in docs:
        sim = np.dot(query, doc) / (q_norm * np.linalg.norm(doc))
        results.append(sim)
    return results

# 方式二:numpy 矩阵运算(一次计算全部)
def cosine_sim_fast(query, docs):
    # 归一化(每个向量除以自身的长度)
    query_norm = query / np.linalg.norm(query)
    docs_norm = docs / np.linalg.norm(docs, axis=1, keepdims=True)
    # 矩阵乘法一次计算所有相似度
    return docs_norm @ query_norm  # @ 是矩阵乘法运算符

import timeit
t1 = timeit.timeit(lambda: cosine_sim_slow(query_vector, doc_vectors), number=5)
t2 = timeit.timeit(lambda: cosine_sim_fast(query_vector, doc_vectors), number=5)
print(f"for 循环: {t1:.2f}s,numpy: {t2:.3f}s,提速 {t1/t2:.0f} 倍")
# 典型输出:for 循环: 2.43s,numpy: 0.038s,提速 64 倍

1.10 实战:文本嵌入预处理管道的完整优化

场景:一个知识库有 50 万条文档需要向量化入库,原始流程耗时超过 2 小时,优化目标是压缩到 15 分钟以内。

1.10.1 优化前(慢版本)

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import pandas as pd
from openai import OpenAI

client = OpenAI()

def embed_pipeline_v1(csv_path: str):
    df = pd.read_csv(csv_path)          # 全量读入内存
    results = []
    for _, row in df.iterrows():        # 逐行遍历(慢)
        text = row["content"]
        # 字符串清洗(逐条)
        text = text.strip().lower()
        # 逐条调用 Embedding API(最慢的地方)
        resp = client.embeddings.create(
            model="text-embedding-3-small",
            input=text
        )
        vector = resp.data[0].embedding
        results.append({
            "id": row["id"],
            "vector": vector
        })
    return results
# 预计耗时:500000 条 × 0.15s/条 ≈ 20 小时

1.10.2 优化后(快版本)

python
# 以下为代码示例,非程序员可跳过代码,重点看文字说明

import pandas as pd
import numpy as np
from openai import OpenAI
import asyncio
import aiohttp
from typing import Generator

client = OpenAI()

# 优化一:向量化字符串清洗
def clean_texts_vectorized(df: pd.DataFrame) -> pd.DataFrame:
    df["content"] = (
        df["content"]
        .str.strip()
        .str.lower()
        .str.replace(r"\s+", " ", regex=True)
        .str[:2000]  # 截断超长文本,避免超出 token 限制
    )
    return df.dropna(subset=["content"])

# 优化二:批量 Embedding
def embed_batch(texts: list[str], batch_size: int = 256) -> list[list[float]]:
    all_embeddings = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        resp = client.embeddings.create(
            model="text-embedding-3-small",
            input=batch
        )
        all_embeddings.extend([d.embedding for d in resp.data])
    return all_embeddings

# 优化三:分块读取大文件(控制内存峰值)
def embed_pipeline_v2(csv_path: str, output_path: str):
    CHUNK_SIZE = 10_000
    BATCH_SIZE = 256
    first_chunk = True

    for chunk in pd.read_csv(csv_path, chunksize=CHUNK_SIZE):
        # 向量化清洗
        chunk = clean_texts_vectorized(chunk)

        # 批量 Embedding
        texts = chunk["content"].tolist()
        vectors = embed_batch(texts, batch_size=BATCH_SIZE)

        # 优化四:用 numpy 转换向量为 float32(节省一半内存)
        vectors_np = np.array(vectors, dtype=np.float32)

        # 写入结果
        chunk["vector"] = vectors_np.tolist()
        chunk[["id", "vector"]].to_csv(
            output_path,
            mode="w" if first_chunk else "a",
            header=first_chunk,
            index=False
        )
        first_chunk = False
        print(f"已处理 {len(chunk)} 条")

# 优化效果对比
# v1:500000 条 × 0.15s = 20小时
# v2:500000 ÷ 256批 × 0.5s/批 ≈ 16分钟(提速 75 倍)

1.10.3 优化效果汇总

优化点 原耗时 优化后耗时 收益
字符串清洗(pandas 向量化) ~15 分钟 ~0.5 分钟 30x
Embedding 调用(批量) ~20 小时 ~16 分钟 75x
内存峰值(分块读取) ~8 GB ~200 MB -97%
向量存储(float32 vs float64) 基准 减半 -50% 内存

1.11 小结:优化工具的使用时机

工具 适用场景 一句话描述
cProfile 不知道哪里慢 找出最耗时的函数
line_profiler 知道哪个函数慢 找出函数内最耗时的行
timeit 验证优化效果 精确测量两段代码的速度差
tracemalloc 内存占用异常增长 找出哪行代码在吃内存
numpy 向量化 有大量数值循环 用数组运算替代 for 循环
批处理 逐条调用 API 合并请求,减少网络往返
生成器 大文件逐行处理 控制内存峰值

1.12 向量化操作在 Embedding 计算中的应用

Embedding(向量嵌入)是 RAG 系统的核心计算。理解如何用 numpy 向量化优化 Embedding 计算,能让 RAG 检索速度提升数十倍。

1.12.1 余弦相似度的向量化实现

python
import numpy as np
import time

# 场景:RAG 系统中,用户提问后,需要从知识库中找到最相似的文档

# 知识库向量(模拟:10000 篇文档,每个向量 1536 维)
doc_vectors = np.random.randn(10_000, 1536).astype(np.float32)
query_vector = np.random.randn(1536).astype(np.float32)

# 方式一:Python for 循环(慢)
def find_top_k_slow(query: np.ndarray, docs: np.ndarray, k: int = 5) -> list[int]:
    """逐个计算余弦相似度,Python 循环"""
    similarities = []
    q_norm = np.linalg.norm(query)

    for i, doc in enumerate(docs):
        # 余弦相似度 = 两向量点积 / (向量1的长度 * 向量2的长度)
        sim = np.dot(query, doc) / (q_norm * np.linalg.norm(doc))
        similarities.append((i, sim))

    similarities.sort(key=lambda x: x[1], reverse=True)
    return [i for i, _ in similarities[:k]]

# 方式二:numpy 矩阵运算(快)
def find_top_k_fast(query: np.ndarray, docs: np.ndarray, k: int = 5) -> list[int]:
    """用矩阵运算一次计算所有余弦相似度"""
    # 归一化查询向量
    query_norm = query / np.linalg.norm(query)

    # 归一化所有文档向量(axis=1 表示按行计算范数)
    docs_norm = docs / np.linalg.norm(docs, axis=1, keepdims=True)

    # 矩阵乘法:一次计算所有相似度(形状 (10000,))
    # docs_norm @ query_norm 等价于对每行做点积
    similarities = docs_norm @ query_norm

    # 找 top-k 的索引
    top_k_indices = np.argpartition(similarities, -k)[-k:]  # 比 argsort 快
    return sorted(top_k_indices.tolist(), key=lambda i: similarities[i], reverse=True)


# 性能测试
start = time.perf_counter()
for _ in range(10):
    find_top_k_slow(query_vector, doc_vectors[:1000])  # 只用1000条避免太慢
slow_time = time.perf_counter() - start

start = time.perf_counter()
for _ in range(10):
    find_top_k_fast(query_vector, doc_vectors)  # 全量10000条
fast_time = time.perf_counter() - start

print(f"Python 循环 (1000条): {slow_time:.3f}s")
print(f"numpy 向量化 (10000条): {fast_time:.3f}s")
# 典型输出:
# Python 循环 (1000条): 0.823s
# numpy 向量化 (10000条): 0.042s  ← 数据量10倍,速度却快20倍

1.12.2 批量 Embedding 的最优实现

python
import numpy as np
from openai import OpenAI
import time

client = OpenAI()

def embed_texts_optimized(texts: list[str], batch_size: int = 256) -> np.ndarray:
    """
    最优化的批量 Embedding 实现:
    1. 批量 API 调用(减少网络往返)
    2. 用 float32 存储(比 float64 节省一半内存)
    3. L2 归一化(便于后续用点积代替余弦相似度,更快)
    """
    all_embeddings = []

    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]

        # 批量调用:一次 API 请求,处理 256 条文本
        response = client.embeddings.create(
            model="text-embedding-3-small",
            input=batch,
            dimensions=512,  # 可以降维减少存储和计算量(原始 1536 维)
        )

        # 提取 embedding 向量列表
        batch_embeddings = [e.embedding for e in response.data]
        all_embeddings.extend(batch_embeddings)

        if (i + batch_size) % 1000 == 0:
            print(f"已处理 {min(i + batch_size, len(texts))}/{len(texts)} 条")

    # 转成 numpy 数组,用 float32 节省内存
    vectors = np.array(all_embeddings, dtype=np.float32)

    # L2 归一化:向量除以自身长度,使每个向量的欧式距离为 1
    # 归一化后,余弦相似度 = 简单的矩阵乘法(不需要除以范数)
    norms = np.linalg.norm(vectors, axis=1, keepdims=True)
    vectors = vectors / norms

    return vectors


# 使用归一化后的向量进行检索(点积代替余弦相似度)
def search_normalized(
    query_vector: np.ndarray,
    doc_vectors: np.ndarray,
    k: int = 5
) -> list[int]:
    """
    对归一化向量,点积 = 余弦相似度,计算更快。
    """
    # 归一化查询向量
    query_norm = query_vector / np.linalg.norm(query_vector)

    # 矩阵乘法(已经归一化,不需要除以范数)
    similarities = doc_vectors @ query_norm

    # 取 top-k
    top_k_indices = np.argpartition(similarities, -k)[-k:]
    return sorted(top_k_indices.tolist(), key=lambda i: similarities[i], reverse=True)

1.12.3 内存估算

当知识库很大时,向量存储的内存消耗值得关注:

python
# 内存占用估算
def estimate_vector_memory(n_docs: int, dimensions: int = 1536) -> dict:
    """估算向量库的内存需求"""
    # float32: 每个数字 4 字节
    # float64: 每个数字 8 字节(更精确,但 AI 任务通常不需要)
    bytes_per_vec_f32 = dimensions * 4
    bytes_per_vec_f64 = dimensions * 8

    total_f32_mb = n_docs * bytes_per_vec_f32 / (1024 * 1024)
    total_f64_mb = n_docs * bytes_per_vec_f64 / (1024 * 1024)

    return {
        "n_docs": n_docs,
        "dimensions": dimensions,
        "float32_mb": round(total_f32_mb, 1),
        "float64_mb": round(total_f64_mb, 1),
        "recommendation": "float32" if total_f32_mb < 1024 else "考虑向量数据库(Chroma/Pinecone)"
    }

# 实际场景
print(estimate_vector_memory(10_000))   # 10000 篇文档
# {'n_docs': 10000, 'dimensions': 1536, 'float32_mb': 58.6, ...}

print(estimate_vector_memory(1_000_000, dimensions=512))  # 100万篇,降维到512
# {'n_docs': 1000000, 'dimensions': 512, 'float32_mb': 1953.1, ...}
# 建议使用向量数据库

1.13 小结

工具 适用场景 一句话描述
cProfile 不知道哪里慢 找出最耗时的函数
line_profiler 知道哪个函数慢 找出函数内最耗时的行
timeit 验证优化效果 精确测量两段代码的速度差
tracemalloc 内存占用异常增长 找出哪行代码在吃内存
numpy 向量化 有大量数值循环 用数组运算替代 for 循环
批量 API 调用 逐条调用 Embedding 合并请求,减少网络往返
float32 向量存储 比 float64 节省一半内存,AI 任务精度够用
L2 归一化 余弦相似度检索 归一化后点积 = 余弦相似度,计算更快

性能优化没有银弹。先测量,再优化,用数据指导决策,而不是靠感觉猜。

本页目录