当前位置: 首页 > news >正文

QTableWidget 中insertRow(0)(头插)和 insertRow(rowCount())(尾插)的性能差异

一、目的

在 Qt 的 QTableWidget 中,insertRow(0) (头插)和 insertRow(rowCount())(尾插)在性能上存在显著差异。


二、QAbstractItemModel:: insertRows 原文解释

QAbstractItemModel Class | Qt Core 5.15.18

![[QAbstractItemModel-insertRows.png]]

AI 解释

QAbstractItemModel:: insertRows 是 Qt 模型/视图框架中用于在模型中插入行的一个虚函数。以下是对该函数的详细解释,包括其功能、参数、返回值以及如何正确实现自定义模型中的插入操作。

函数声明

[virtual] bool QAbstractItemModel::insertRows(int row, int count, const QModelIndex &parent = QModelIndex())

功能概述

insertRows 函数用于在模型中指定位置插入一行或多行数据。具体来说,它会在给定的 row 位置之前插入 count 行新数据。这些新行将成为由 parent 模型索引表示的父项的子项。

参数说明

  • int row
    指定插入行的位置。如果 row0,新行将被插入到父项的最前面;如果 row 等于 rowCount(parent),新行将被追加到父项的最后面。
  • int count
    要插入的行数。可以一次性插入多行数据。
  • const QModelIndex &parent = QModelIndex()
    表示插入行的父索引。如果 parent 是无效索引(默认值),则插入的行将成为顶级行(即没有父项)。否则,新行将作为 parent 所表示的父项的子项插入。

返回值

  • bool
    返回 true 表示插入操作成功;返回 false 表示插入操作失败。

注意事项

  1. 基类实现
    QAbstractItemModel 的基类实现是空的,直接调用会返回 false。因此,如果你的自定义模型需要支持插入行的操作,必须重写(reimplement)此函数。
  2. 通知机制
    在实际插入数据之前和之后,必须调用 beginInsertRowsendInsertRows 函数。这两个函数用于通知视图和其他相关组件模型数据的变化,以确保界面能够正确更新。
  3. 实现逻辑
    当重写 insertRows 时,除了调用 beginInsertRowsendInsertRows 来通知变化外,还需要在内部数据结构中实际插入新的数据行。

示例代码

以下是一个简单的自定义模型中重写 insertRows 的示例:

#include <QAbstractItemModel>
#include <QModelIndex>
#include <QVariant>
#include <QList>

class MyModel : public QAbstractItemModel {
    Q_OBJECT

public:
    // 构造函数和其他必要的成员函数...

    // 重写 insertRows 函数
    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
        // 开始插入操作的通知
        beginInsertRows(parent, row, row + count - 1);

        // 在内部数据结构中插入新行
        if (!parent.isValid()) {
            // 插入顶级行
            for (int i = 0; i < count; ++i) {
                m_data.insert(row, QVariant()); // 假设 m_data 是 QList<QVariant>
            }
        } else {
            // 插入子行(假设父项有子项列表)
            MyItem *parentItem = static_cast<MyItem*>(parent.internalPointer());
            if (parentItem) {
                parentItem->appendChildItems(count); // 假设有相应的方法添加子项
            }
        }

        // 结束插入操作的通知
        endInsertRows();

        return true; // 插入成功
    }

    // 其他必要的虚函数实现...

private:
    QList<QVariant> m_data; // 示例数据结构,实际应用中可能更复杂
};

详细解释

  1. 调用 beginInsertRows

    beginInsertRows(parent, row, row + count - 1);
    

    这个函数通知所有连接到模型的视图,即将在 parent 父索引下的 rowrow + count - 1 位置插入新行。这使视图能够预先调整其布局和缓冲区,以适应即将到来的数据变化。

  2. 修改内部数据结构
    根据 parent 是否有效,决定是插入顶级行还是子行:

    • 顶级行:直接在 m_data 列表的指定位置插入新数据。
    • 子行:通过 parent 获取对应的父项对象,并调用相应的方法在父项下插入新的子项。
  3. 调用 endInsertRows

    endInsertRows();
    

    这个函数通知视图插入操作已完成,视图可以更新显示新的数据行。

  4. 返回值
    返回 true 表示插入操作成功。如果插入过程中出现错误,可以返回 false,但通常需要提供适当的错误处理机制。

使用场景

  • 动态添加数据:当用户通过界面按钮或其他方式请求添加新行时,模型通过 insertRows 插入新数据。
  • 批量插入:一次性插入多行数据,提高效率。
  • 层级结构:在树形结构模型中,为特定父项插入子行。

性能考虑

  • 通知开销:频繁调用 beginInsertRowsendInsertRows 可能导致性能下降。因此,应尽量批量插入数据,而不是逐行插入。
  • 数据结构的效率:确保内部数据结构支持高效的插入操作。例如,使用链表可以快速在头部插入,而动态数组在尾部插入更高效。

总结

QAbstractItemModel::insertRows 是实现自定义模型时用于插入新行的关键函数。通过正确重写此函数,并结合 beginInsertRowsendInsertRows 的通知机制,可以确保模型数据的动态变化能够正确反映在关联的视图上,同时保持良好的性能和用户体验。


三、QTableWidget::insertRow(int row) 源码

路径

  • Qt\5.15.2\Src\qtbase\src\widgets\itemviews\qtablewidget. cpp
  • Qt\5.15.2\Src\qtbase\src\widgets\itemviews\qtablewidget_p.h

代码解析

void QTableWidget::insertRow(int row)
{
    Q_D(QTableWidget);
    d->tableModel()->insertRows(row);
}

bool QTableModel::insertRows(int row, int count, const QModelIndex &)
{
    if (count < 1 || row < 0 || row > verticalHeaderItems.count())
        return false;

    beginInsertRows(QModelIndex(), row, row + count - 1);
    int rc = verticalHeaderItems.count();
    int cc = horizontalHeaderItems.count();
    verticalHeaderItems.insert(row, count, 0);
    if (rc == 0)
        tableItems.resize(cc * count);
    else
        tableItems.insert(tableIndex(row, 0), cc * count, 0);
    endInsertRows();
    return true;
}

class QTableModel : public QAbstractTableModel
{
	Q_OBJECT
	friend class QTableWidget;
	.....
private:
	const QTableWidgetItem *prototype;
	QVector<QTableWidgetItem*> tableItems;
	QVector<QTableWidgetItem*> verticalHeaderItems;
	QVector<QTableWidgetItem*> horizontalHeaderItems;
	// A cache must be mutable if get-functions should have const modifiers
	mutable QModelIndexList cachedIndexes; };

从这段代码可以看出,QTableWidget 的插入行操作实际上是通过其内部的 QTableModel 来实现的,具体步骤包括:

  1. 插入表头项verticalHeaderItems.insert(row, count, 0);
  2. 插入表格数据项
    • 如果当前表格没有行 (rc == 0),则直接调整 tableItems 的大小。
    • 否则,在指定位置插入新的数据项,tableItems.insert(tableIndex(row, 0), cc * count, 0);

性能差异分析

1. 性能差异的核心原因

头插 (insertRow(0))
  • 表头项操作
    verticalHeaderItems.insert(row, count, 0)QVector 头部插入元素,需要将后续所有元素向后移动,时间复杂度为 ​O (n)(n 为当前行数)
  • 表格数据操作
    tableItems.insert(tableIndex(row, 0), cc * count, 0) 在数据数组头部插入新元素,同样需要移动后续所有数据,时间复杂度为 ​O (n * m)(m 为列数)
  • 视图更新
    触发 beginInsertRowsendInsertRows,通知视图重新计算所有行的位置,导致界面重绘开销较大。
尾插 (insertRow(rowCount()))
  • 表头项操作
    QVector 尾部追加元素,时间复杂度为 ​O (1)(假设预分配了足够内存)
  • 表格数据操作
    • 若表格为空,通过 tableItems.resize(cc * count) 初始化内存(O (1))。
    • 若表格非空,直接追加到 QVector 末尾(O (1))。
  • 视图更新
    视图仅需扩展显示区域,渲染开销更低。

2. 插入操作的时间复杂度

  • 尾插 (insertRow(rowCount()))
    • 表头项插入:在 QVector 的末尾插入元素,时间复杂度为 ​O(1)
    • 表项数据插入:如果表格为空,仅需调整大小;否则,由于是在末尾插入,QVector::insert 在有预留空间的情况下也是 ​O(1)。但如果有重新分配内存的需求,可能会涉及到 ​O(n) 的复制操作,但这种情况在尾部插入时较少发生。
  • 头插 (insertRow(0))
    • 表头项插入:在 QVector 的开头插入元素,需要移动所有现有元素,时间复杂度为 ​O(n)
    • 表项数据插入:同样,在开头插入需要移动所有现有的数据项,时间复杂度为 ​O(n)。如果表格较大,这种移动操作的开销会显著增加。

3. 实际性能影响

  • 数据量较小
    • 当表格中的行数较少(例如几十行)时,头插和尾插的性能差异可能不明显,用户几乎感觉不到延迟。
  • 数据量较大
    • 当表格包含数千行甚至更多行时,头插操作由于需要频繁移动大量元素,会导致明显的性能下降,甚至可能造成界面卡顿或响应延迟。
    • 尾插操作由于主要在末尾添加元素,性能相对稳定,几乎不受插入次数的影响。

4. 内存与缓存的影响

  • 内存重新分配
    • QVector 在插入元素时,如果当前容量不足以容纳新元素,会进行内存重新分配和元素复制。头插操作由于频繁移动元素,可能更频繁地触发内存重新分配,增加开销。
  • 缓存局部性
    • 尾插操作更有利于 CPU 缓存的利用,因为新元素通常被添加到内存的连续区域。而头插操作打乱了数据的连续性,导致缓存命中率降低,进一步影响性能。

5. 实际性能对比

操作类型时间复杂度内存移动次数适用场景性能影响
头插O(n)高(n 次移动)按倒序插入少量数据高(避免频繁使用)
尾插O(1)低(尾部追加)常规数据追加、大规模插入低(推荐优先使用)
  • 小数据量场景​(如数十行):两者差异可忽略。
  • 大数据量场景​(如数万行): - 头插单行耗时可能是尾插的 ​10 倍以上​(因需移动全部数据)。 - 尾插性能稳定,适合高频插入操作。

四、​ 示例测试

以下是一个使用 QElapsedTimerQTableWidget 测试 insertRow(0)(头插)和 insertRow(rowCount())(尾插)性能差异的完整示例代码。代码通过批量插入数据并测量时间,直观展示两者的性能差距:

#include <QApplication>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QElapsedTimer>
#include <QTimer>
#include <QDebug>

// 测试函数:插入指定行数到表格的头部或尾部
void testInsertPerformance(QTableWidget* table, int rowCount, const QString& testName) {
	table->clearContents();
	table->setRowCount(0);

	// 禁用视图更新以提高测试准确性
	table->setUpdatesEnabled(false);

	QElapsedTimer timer;
	timer.start();

	for (int i = 0; i < rowCount; ++i) {
		int row = (testName == "Head Insert") ? 0 : table->rowCount();
		table->insertRow(row);
		table->setItem(row, 0, new QTableWidgetItem(QString("Name %1").arg(i)));
		table->setItem(row, 1, new QTableWidgetItem(QString::number(i)));
		table->setItem(row, 2, new QTableWidgetItem(QString("City %1").arg(i % 10)));
	}

	auto elapsed_ms = timer.elapsed();
	table->setUpdatesEnabled(true); // 恢复视图更新

	qDebug() << testName << "Performance:";
	qDebug() << "  Time elapsed:" << elapsed_ms << "ms";
	qDebug() << "  Time elapsed:" << elapsed_ms << "ms";
	qDebug() << "  Memory used:" << table->sizeHint().height() * sizeof(QTableWidgetItem*) / 1024.<< "KB";
	qDebug() << "  Memory used:" << table->sizeHint().width() * sizeof(QTableWidgetItem*) / 1024.<< "KB";
}

int main(int argc, char* argv[]) {
	QApplication a(argc, argv);

	// 创建测试表格(3列)
	QTableWidget table;
	table.setColumnCount(3);
	table.setHorizontalHeaderLabels({ "Name", "Age", "City" });
	table.resize(600, 400);
	table.show();
#if 1
	// 测试头插性能(插入100000行)
	testInsertPerformance(&table, 100000, "Head Insert");

	// 等待用户操作后测试尾插性能
	QTimer::singleShot(2000, [&]() {
		testInsertPerformance(&table, 100000, "Tail Insert");
		});
#else
	//测试尾插性能
	testInsertPerformance(&table, 100000, "Tail Insert");
	// 等待用户操作后测试头插性能
	QTimer::singleShot(2000, [&]() {
		testInsertPerformance(&table, 100000, "Head Insert");
		});
#endif
	return a.exec();
}

代码解析与测试结果

1. ​核心逻辑
  • 禁用视图更新:通过 setUpdatesEnabled(false) 暂停界面刷新,避免渲染开销干扰时间测量。
  • 批量插入数据:循环插入指定行数,每行包含姓名、年龄、城市三列数据。
  • 时间测量:使用 QElapsedTimer 记录插入操作的耗时。
  • 内存估算:通过 sizeHint().height() 估算表格占用的内存(粗略计算)。
2. ​测试结果示例

![[100000performance.png]]

操作类型插入行数耗时(ms)​内存占用(KB)​
头插100,0009,2241.5
尾插100,00025091.5

:实际结果可能因硬件和 Qt 版本略有差异,但头插耗时通常比尾插高 ​。


性能差异原因

  1. 头插 (insertRow(0))

    • 数据移动QVector 在头部插入元素需移动后续所有元素,时间复杂度为 ​O(n)
    • 内存重新分配:频繁插入导致内存多次重新分配,增加开销。
  2. 尾插 (insertRow(rowCount()))

    • 尾部追加QVector 在尾部插入元素时间复杂度为 ​O(1)(预分配内存时)
    • 内存连续性:数据连续存储,缓存命中率高。

优化建议

  1. 预分配内存:若已知数据量,提前调用 table->setRowCount(rowCount) 预分配内存。
  2. 改用自定义模型:对超大数据集,使用 QAbstractTableModel 替代 QTableWidget,通过虚拟化技术减少内存占用。

五、​ 优化建议

基于上述分析,以下是一些优化建议:

  1. 优先使用尾插

    • 如果业务逻辑允许,尽量使用 insertRow(rowCount()) 进行尾插操作,以获得更好的性能表现。
  2. 批量插入

    • 如果需要插入多行,尽量一次性批量插入,而不是逐行插入。例如,先收集所有需要插入的数据,然后调用一次 setRowCount
    // 示例:批量插入多行
    tableWidget->setRowCount(10000);
    for (int i = 0; i < 10000; ++i) {
        // 填充数据
    }
    
  3. 使用模型/视图架构

    • 如果需要频繁进行插入、删除等操作,考虑直接使用 QAbstractTableModel 或其子类 QStandardItemModel,而不是 QTableWidgetQAbstractTableModel 提供了更高的灵活性和性能优化空间,尤其是在处理大规模数据时。
    • 对于超大数据集,推荐使用 QTableView + 自定义 QAbstractTableModel / QStandardItemModel,通过虚拟化技术减少内存和渲染开销。例如:
    class CustomTableModel : public QAbstractTableModel {
        // 实现 data()、rowCount()、columnCount() 等虚函数
    };
    QTableView *tableView = new QTableView;
    tableView->setModel(new CustomTableModel);
    // 或者
    tableView->setModel(new QStandardItemModel);
    
  4. 延迟更新

    • 若必须头插,可先禁用视图更新(setUpdatesEnabled(false)),插入多行后再统一刷新界面:
    tableWidget->setUpdatesEnabled(false);
    for (int i = 0; i < 100; ++i) {
        tableWidget->insertRow(0); // 批量头插
    }
    tableWidget->setUpdatesEnabled(true);
    
    • 在进行大量插入操作前,可以暂时禁用视图的更新,操作完成后再恢复。这可以通过 setUpdatesEnabled(false)setUpdatesEnabled(true) 实现,但需要注意处理好数据的一致性。
    tableWidget->setUpdatesEnabled(false);
    // 执行批量插入操作
    tableWidget->setUpdatesEnabled(true);
    
  5. 懒加载技术

    • 仅在可见区域加载数据,结合滚动事件动态插入行(参考 QT 懒加载技术 的 UpdateAlarmList 实现):
    void AlarmCenter::wheelEvent(QWheelEvent *event) {
        // 根据滚动条位置动态加载数据
        int visibleRows = tableViewHeight / rowHeight;
        int startRow = currentRow - visibleRows / 2;
        UpdateAlarmList(startRow, visibleRows);
    }
    
  6. 性能对比总结

指标头插尾插
时间复杂度O (n)(数据移动)O (1)(尾部追加)
内存操作高(频繁重新分配和复制)低(预分配或追加)
界面渲染高(全表重绘)低(仅扩展区域)
适用场景倒序插入少量数据常规追加、大规模插入

总结

  • 头插 (insertRow(0))
    • 性能较低,尤其是在数据量较大时,由于需要在开头插入元素,导致所有现有元素需要移动,时间复杂度为 ​O(n)
  • 尾插 (insertRow(rowCount()))
    • 性能较高,在末尾插入元素通常为 ​O(1),即使有内存重新分配,也相对高效。

因此,在使用 QTableWidget::insertRow 时,​尾插性能远优于头插,应优先考虑尾插操作,以获得更好的性能表现。如果业务逻辑确实需要频繁进行头插操作,建议重新评估设计,或者考虑使用更适合频繁插入删除操作的模型/视图架构(如 QAbstractTableModel)配合自定义的数据结构优化性能。​

http://www.dtcms.com/a/109615.html

相关文章:

  • 服务器磁盘io性能监控和优化
  • c++中cin.ignore()的作用
  • Unirest:优雅的Java HTTP客户端库
  • CUDA概览
  • Python星球日记 - 第1天:欢迎来到Python星球
  • 十款Steam单机游戏
  • 2025-04-03 Latex学习1——本地配置Latex + VScode环境
  • PandasAI:当数据分析遇上自然语言处理
  • uni-app项目上传至gitee方法详细教程
  • Java代理(六)当前主流动态代理框架性能对比
  • 安全、可靠,企业内部im即时通讯软件选择
  • 十一、buildroot系统登录配置
  • 从0开始的构建的天气预报小时钟(基于STM32F407ZGT6,ESP8266 + SSD1309)——第1章 简单的介绍一下ESP8266和他的编程指令
  • Oracle数据库数据编程SQL<6.2 数据字典表之间的关联关系>
  • C++的智能指针weak_ptr和普通指针的区别
  • 第五课:高清修复和放大算法
  • MySQL安装教程(详细版)
  • Linux应用编程(文件IO)
  • 移远RG200U-CN模组WAKEUP_IN引脚
  • SAP ABAP AVL单元格颜色
  • 问题解决:glog中的LOG(INFO)与VLOG无法打印
  • 每日一题(小白)分析娱乐篇10
  • DDD与MVC扩展能力对比
  • Agent TARS与Manus的正面竞争
  • THUNLP_Multimodal_Excercise
  • Java - WebSocket配置及使用
  • Dart 语法
  • 【Tauri2】013——前端Window Event与创建Window
  • 搭建环境-opencv-qt
  • 震源车:震源激发平板模态分析