常见瓶颈分类
| 类型 | 表现 | 定位工具 |
|---|
| CPU | 界面卡顿、数据处理慢 | perf / VTune / QElapsedTimer |
| 绘制 | 滚动不流畅、缩放掉帧 | Qt 帧率监控 |
| 内存 | OOM、swap、堆碎片 | valgrind / heaptrack |
| IO | 启动慢、读写卡顿 | strace / QIODevice 耗时 |
| 信号槽 | 大量信号场景响应慢 | QElapsedTimer 埋点 |
CPU 瓶颈 — 绘制是头号杀手
禁用不必要的抗锯齿
// 绘制 10000 条线段(如实时曲线)
// 开启抗锯齿: ~200ms(掉帧到 5FPS)
// 关闭抗锯齿: ~5ms(60FPS 无压力)
p.setRenderHint(QPainter::Antialiasing, false);
p.drawPolyline(polyline);
QPolygonF > QPainterPath(折线场景)
// ❌ QPainterPath — 即使全直线也走贝塞尔管线,慢 3-5 倍
QPainterPath path;
for (auto &pt : points) { path.lineTo(pt); }
p.drawPath(path);
// ✅ QPolygonF — 纯折线,硬件加速友好的数据结构
QPolygonF poly;
for (auto &pt : points) { poly << pt; }
p.drawPolyline(poly);
每帧开销检查清单
void paintEvent(QPaintEvent *e) override {
// ☐ QPixmap 是否复用?(成员变量,只在 resize 时重建)
// ☐ Pen/Brush/Font 是否复用?(成员变量,不在 paintEvent 内创建)
// ☐ 是否裁剪到 damage rect?(p.setClipRect(e->rect()))
// ☐ 是否做了抽稀?(1px 内的点合并 → 减少 drawPolyline 数据量)
// ☐ 文字绘制是否缓存?(staticText 替代 drawText)
}
内存瓶颈
QPixmap 缓存策略
// ❌ 每次 paintEvent 重建 QPixmap → 每帧 malloc + free 3.8MB
void paintEvent(QPaintEvent *) {
QPixmap offscreen(size()); // 16ms 后析构 → 碎片累积
}
// ✅ 成员变量复用:只在 resizeEvent 重建
void resizeEvent(QResizeEvent *e) override {
m_offscreen = QPixmap(e->size());
}
QObject 父子关系与内存
// ✅ 给 parent — Qt 自动管理生命周期
auto *btn = new QPushButton("OK", this); // this 析构时自动 delete btn
// ❌ 漏设 parent + 忘记 delete → 内存泄漏
auto *btn = new QPushButton("OK");
// ⚠️ moveToThread 的 worker 不能有 parent
Worker *w = new Worker; // 无 parent
w->moveToThread(thread);
// Qt 不允许 parent 和自身不在同一线程的 QObject
IO 瓶颈
SQLite 写入优化
// ❌ 每条 INSERT 单独提交 — 1000 次 fsync
for (auto &dp : dataPoints)
query.exec("INSERT INTO ..."); // 每次都是隐式事务
// ✅ 批量事务 — 1 次 fsync
db.transaction();
for (auto &dp : dataPoints) {
query.addBindValue(dp.value);
query.exec();
}
db.commit();
// 性能提升 10-50 倍
// ✅ WAL 模式 — 读写并发
db.exec("PRAGMA journal_mode=WAL");
文件 IO 异步化
// ❌ 主线程同步写文件 → UI 冻结
void saveReport() {
QFile f("report.csv");
f.open(QIODevice::WriteOnly);
f.write(largeData); // 可能 500ms → UI 卡死
}
// ✅ 异步写
void saveReport() {
auto *worker = new FileWriter("report.csv", largeData);
QThread *thread = new QThread;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &FileWriter::write);
connect(worker, &FileWriter::finished, thread, &QThread::quit);
connect(worker, &FileWriter::finished, this, &MainWindow::onSaved);
thread->start();
}
信号槽性能
// 信号槽调用开销 ≈ 函数指针的 5-10 倍
// 100Hz 数据流每秒 100 次 signal → 开销可忽略
// 100kHz 传感器每秒 100,000 次 signal → 考虑直接函数调用
// DirectConnection 比 QueuedConnection 快 3-5 倍
// 但跨线程必须用 QueuedConnection
connect(sender, &Sender::dataReady,
receiver, &Receiver::onData,
Qt::DirectConnection); // 同线程 → 快
避免信号风暴
// ❌ 大批量更新触发 N 次信号
for (auto &row : hugeData)
model->appendRow(row); // 每次 emit layoutChanged → 整个 View 重绘
// ✅ 批量操作后发一次信号
model->beginResetModel();
for (auto &row : hugeData)
model->appendRowSilently(row); // 内部追加,不发射信号
model->endResetModel(); // 一次性通知 View 刷新
Profiling 工具链
| 工具 | 用途 | 命令 |
|---|
QElapsedTimer | 函数耗时埋点 | 代码内嵌 |
valgrind --tool=callgrind | CPU 调用图 | valgrind --tool=callgrind ./app |
heaptrack | 内存分配热点 | heaptrack ./app |
perf | Linux 采样 Profiling | perf record -g ./app |
| Qt 内置日志 | 绘制帧率 | QT_LOGGING_RULES=qt.qpa.*=true |
QElapsedTimer 快速埋点
#include <QElapsedTimer>
QElapsedTimer t; t.start();
doExpensiveWork();
qDebug() << "doExpensiveWork:" << t.elapsed() << "ms";
// 热点定位三步:
// 1. 先找最慢的大函数 → QElapsedTimer
// 2. 再进函数内找慢的循环/算法 → 代码审查
// 3. 最后用 perf/callgrind 找指令级热点
优化优先级
1. 绘制优化 ← 收益最大(卡顿的 80% 原因)
抗锯齿/数据结构/QPixmap 复用/抽稀
2. IO 异步化 ← 阻塞主线程的直接原因
文件读写踢到 Worker 线程 / SQLite WAL + 批量事务
3. 信号槽批量 ← 高频场景才需要
beginResetModel / 直接函数调用 / 信号合并
4. 内存复用 ← 避免碎片
QPixmap/Pen/Brush 成员变量复用
5. 算法优化 ← 最后才做
只有在 Profiling 确认算法是瓶颈时才优化