STL 性能优化实战:解决项目中标准模板库的性能瓶颈

🧑 博主简介:CSDN博客专家、全栈领域优质创作者、高级开发工程师、高级信息系统项目管理师、系统架构师,数学与应用数学专业,10年以上多种混合语言开发经验,从事DICOM医学影像开发领域多年,熟悉DICOM协议及其应用开发技术。我的技能涵盖了多种编程语言和技术框架:作为高级C/C++与C#开发工程师,擅长Windows系统下的.NET及C++开发技术,尤其精通MFC、DLL动态链接库、WinForm、WPF、Windows服务、WebAPI及.NET Core跨平台等技术的开发工作。熟悉Java开发,并利用业余时间学习了JavaScript、Vue等前端技术,同时自学了QT开发工具,对Python开发也有一定的了解,因此使我具备了使用多种混合语言进行开发的能力。我一直坚持撰写博客文章,记录个人的学习历程,分享编程开发相关的知识与经验,旨在为编程爱好者提供帮助和支持。通过这样的方式,我希望可以与志同道合的朋友交流探讨,共同进步,在技术的世界里不断学习和成长。如果您也热衷于技术探索,愿意一起讨论最新技术趋势或解决遇到的技术难题,欢迎随时联系。让我们携手共进,在追求卓越技术的道路上越走越远。欢迎关注、学习及合作,可提供解决方案和技术支持!
技术合作请加本人wx(注明来自csdn):xt20160813

在这里插入图片描述

STL 性能优化实战:解决项目中标准模板库的性能瓶颈

在现代 C++ 编程中,标准模板库(STL)是开发者不可或缺的工具。它提供了丰富的容器、算法和迭代器,极大地简化了代码编写的复杂性。然而,尽管 STL 提供了强大的功能,但不当使用也可能导致显著的性能瓶颈,尤其是在大型或对性能要求严格的项目中。本篇博客将深入探讨 STL 的性能优化策略,通过实际案例和详细示例,帮助开发者有效识别并解决 STL 在项目中的性能瓶颈问题。
在这里插入图片描述

目录

  1. STL 性能概述
  2. 识别性能瓶颈
    • 常用的性能分析工具
    • 常见的 STL 性能问题
  3. 性能优化策略
    • 1. 选择合适的容器
    • 2. 减少内存分配
    • 3. 避免不必要的拷贝
    • 4. 利用移动语义和原位构造
    • 5. 优化算法选择
    • 6. 提高缓存局部性
    • 7. 使用自定义分配器
  4. 实战案例:优化高性能日志系统
    • 初始实现
    • 优化步骤
    • 优化后的实现
  5. 最佳实践与总结

STL 性能概述

在深入优化 STL 性能之前,有必要理解 STL 各组成部分的基本性能特征:

容器的时间复杂度

每种 STL 容器都有其特定的时间复杂度特性,这直接影响到它在不同场景下的性能表现。例如:

  • std::vector:在尾部插入和访问随机位置元素时具有 O(1) 的时间复杂度,但在中间插入或删除元素时需要 O(n) 时间。
  • std::list:在任意位置插入和删除元素时具有 O(1) 的时间复杂度,但不支持高效的随机访问。
  • std::map / std::unordered_mapstd::map 基于平衡二叉树实现,查找、插入、删除操作平均为 O(log n),而 std::unordered_map 基于哈希表实现,平均为 O(1)(最坏情况为 O(n))。
    在这里插入图片描述

算法的性能

STL 提供了丰富的算法库,选择合适的算法对性能有着重要影响。例如,std::sort 的复杂度为 O(n log n),而 std::stable_sort 也是 O(n log n),但常数因子更大。

迭代器的使用

不同类型的迭代器(输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器)在性能上有不同影响。比如,随机访问迭代器支持常数时间的跳转,而双向迭代器则需要线性时间。

理解这些基本概念是优化 STL 性能的前提。

识别性能瓶颈

在优化之前,必须首先识别出性能瓶颈所在。没有正确的性能分析,任何优化都是盲目的。
在这里插入图片描述

常用的性能分析工具

以下是几种常用的性能分析工具,可以帮助开发者定位程序中的性能问题:

  • gprof:GNU 的性能分析器,适用于 Linux 环境。
  • Valgrind (Callgrind):一个强大的程序分析工具,尤其适用于检测程序的性能热点。
  • Visual Studio Profiler:集成在 Visual Studio 中的性能分析工具,适用于 Windows 开发环境。
  • Perf:Linux 系统下的性能分析工具,功能强大。
使用 gprof 进行性能分析的示例
  1. 编译时启用分析选项

    g++ -pg -O2 -o my_app my_app.cpp
    
  2. 运行应用程序

    ./my_app
    
  3. 生成分析报告

    gprof my_app gmon.out > analysis.txt
    
  4. 分析 analysis.txt 以识别性能热点

常见的 STL 性能问题

通过分析,以下是一些在项目中常见的 STL 性能问题:

  • 频繁的内存分配:例如,频繁向 std::vector 中插入元素导致多次分配和拷贝。
  • 不必要的拷贝:传递或存储对象时未使用引用或移动,从而导致不必要的拷贝开销。
  • 选择不当的容器:在需要频繁插入或删除的场景中使用 std::vector 而不是 std::list 等更合适的容器。
  • 算法选择不当:例如,在已排序的数据集中使用线性搜索而不是二分搜索。
    在这里插入图片描述

性能优化策略

以下几种策略可以显著提升 STL 的性能:

1. 选择合适的容器

选择适合特定使用场景的容器是提升性能的第一步。不同的容器在不同操作上的表现差异巨大。

常见容器比较

操作std::vectorstd::liststd::dequestd::map / std::unordered_map
随机访问O(1)不支持O(1)不支持
中间插入/删除O(n)O(1)O(n)-
尾部插入/删除O(1) 摊销O(1)O(1)-
查找O(n)O(n)O(n)O(log n) / O(1)

示例:std::vector vs std::list

假设需要在集合中频繁地在中间插入元素:

#include <vector>
#include <list>
#include <algorithm>void insert_elements_vector(std::vector<int>& vec, const std::vector<int>& elements) {for (const auto& el : elements) {vec.insert(vec.begin() + vec.size() / 2, el); // 每次插入中间位置,O(n) 时间}
}void insert_elements_list(std::list<int>& lst, const std::vector<int>& elements) {auto it = lst.begin();std::advance(it, lst.size() / 2);for (const auto& el : elements) {lst.insert(it, el); // 每次插入中间位置,O(1) 时间}
}

优化启示:如果应用在中间频繁插入或删除元素,使用 std::list 可以提供更好的性能。然而,对于大多数其他用例,std::vector 因其更好的缓存局部性和更低的内存开销,表现更优。

2. 减少内存分配

动态内存分配是昂贵的,减少内存分配的次数和开销可以显著提升性能。

优化技巧

  • 预留空间(Reserve):在容器已知大小或可以估算时,预先分配足够的内存,避免多次重新分配。
  • 合理缩减(Shrink-to-Fit):在容器大小大幅变化时,适时缩减容器容量,减少内存占用。

示例:使用 reserve 优化 std::vector

#include <vector>
#include <iostream>int main() {std::vector<int> vec;vec.reserve(1000); // 预先分配 1000 个元素的空间for (int i = 0; i < 1000; ++i) {vec.emplace_back(i);}std::cout << "Vector size: " << vec.size() << std::endl;return 0;
}

优势:通过预留空间,可以避免 std::vector 在插入元素时多次重新分配内存,从而提升性能。

3. 避免不必要的拷贝

拷贝大型对象会带来显著的性能开销,使用引用、指针或移动语义可以避免这些开销。

示例:函数参数传递时避免拷贝

#include <vector>// 不优化的版本:传值,会复制整个 vector
void process_vector(std::vector<int> vec) {// 处理代码
}// 优化后的版本:传递常量引用,避免拷贝
void process_vector(const std::vector<int>& vec) {// 处理代码
}

优势:通过传递引用,可以避免在函数调用过程中复制整个容器,从而节省大量时间和内存。

4. 利用移动语义和原位构造

C++11 引入的移动语义和原位构造(emplacement)功能,可以减少临时对象的创建和拷贝操作。

示例:使用 std::moveemplace_back

#include <vector>
#include <string>int main() {std::vector<std::string> vec;std::string temp = "Hello, World!";// 拷贝vec.push_back(temp); // 复制 temp// 移动vec.push_back(std::move(temp)); // 移动 temp// 原位构造vec.emplace_back("直接构造字符串"); // 直接在 vector 中构造字符串return 0;
}

优势:移动语义和原位构造可以减少不必要的拷贝操作,特别是对于复杂或大型对象,能显著提升性能。

5. 优化算法选择

选择合适的 STL 算法,结合合适的数据结构,可以显著提升性能。

示例:使用二分搜索替代线性搜索

#include <vector>
#include <algorithm>int main() {std::vector<int> sorted_vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 线性搜索auto it = std::find(sorted_vec.begin(), sorted_vec.end(), 7); // O(n) 时间// 二分搜索bool found = std::binary_search(sorted_vec.begin(), sorted_vec.end(), 7); // O(log n) 时间return 0;
}

优化启示:在数据已经排序的情况下,使用 std::binary_search 这种时间复杂度更低的算法,可以显著提升查找性能。

6. 提高缓存局部性

数据结构的内存布局对缓存局部性有直接影响,进而影响性能。尽量选择内存连续布局的容器,如 std::vector,可以提高缓存命中率,加快访问速度。

示例:使用 std::vector 进行迭代处理

#include <vector>
#include <list>void process_vector(std::vector<int>& vec) {for(auto& el : vec) {// 处理元素}
}void process_list(std::list<int>& lst) {for(auto& el : lst) {// 处理元素}
}

优势std::vector 的连续存储特性使其在迭代过程中利用了缓存预取机制,从而比基于节点的 std::list 拥有更快的迭代速度。

7. 使用自定义分配器

STL 容器默认使用全局分配器,适用于通用情况。但在特定场景下,自定义分配器可以优化内存分配模式,减少碎片和分配开销。

示例:简单的内存池分配器

#include <memory>
#include <vector>template <typename T>
struct PoolAllocator {using value_type = T;PoolAllocator() = default;template <typename U>PoolAllocator(const PoolAllocator<U>&) {}T* allocate(std::size_t n) {// 简单的内存池分配,实际实现应更复杂return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, std::size_t) noexcept {::operator delete(p);}
};int main() {std::vector<int, PoolAllocator<int>> vec;vec.reserve(1000); // 使用自定义分配器预分配空间// 执行其他操作return 0;
}

优势:自定义分配器可以针对特定的内存使用模式进行优化,如固定大小分配、多线程分配等,从而提升整体性能。

实战案例:优化高性能日志系统

为了更直观地展示 STL 性能优化策略的应用,以下将以一个高性能日志系统为例,详细说明优化过程。

初始实现

假设有一个简单的日志系统,用于实时记录事件:

#include <vector>
#include <string>
#include <mutex>struct LogEntry {int id;std::string message;
};class Logger {
public:void log(int id, const std::string& message) {std::lock_guard<std::mutex> lock(mutex_);logs_.emplace_back(LogEntry{id, message});}const std::vector<LogEntry>& get_logs() const { return logs_; }private:std::vector<LogEntry> logs_;mutable std::mutex mutex_;
};

潜在问题

  1. 无限增长logs_ 容器可能无限增长,导致内存问题。
  2. 线程争用:高频率的日志记录导致多个线程争用锁,影响性能。
  3. 字符串拷贝开销:传递和存储字符串时的拷贝操作可能带来额外开销。

优化步骤

针对上述问题,可以采取以下优化措施:

1. 预先分配内存

通过 reserve 预先分配足够的空间,避免多次内存重新分配。

Logger() {logs_.reserve(1000000); // 预先分配空间,避免多次 reallocation
}
2. 减少锁争用

使用线程本地存储(Thread-Local Storage)或分离的日志缓冲区,减少多个线程间对同一锁的争用。

使用线程本地缓冲区

#include <thread>
#include <vector>
#include <string>
#include <mutex>struct LogEntry {int id;std::string message;
};class Logger {
public:void log(int id, std::string&& message) {auto& buffer = get_buffer();buffer.emplace_back(LogEntry{ id, std::move(message) });}std::vector<LogEntry> collect_logs() {std::vector<LogEntry> all_logs;std::lock_guard<std::mutex> lock(mutex_);for(auto& buffer : buffers_) {all_logs.insert(all_logs.end(),std::make_move_iterator(buffer.begin()),std::make_move_iterator(buffer.end()));buffer.clear();}return all_logs;}private:static const int THREAD_COUNT = 4;std::vector<LogEntry> buffers_[THREAD_COUNT];std::mutex mutex_;std::vector<LogEntry>& get_buffer() {size_t thread_id = std::hash<std::thread::id>()(std::this_thread::get_id()) % THREAD_COUNT;return buffers_[thread_id];}
};

说明:通过为每个线程分配独立的日志缓冲区,避免多个线程对同一个锁的争用,提升日志记录性能。

3. 避免字符串拷贝

利用移动语义减少字符串拷贝开销。

void log(int id, std::string message) { // 按值传递,允许移动std::lock_guard<std::mutex> lock(mutex_);logs_.emplace_back(LogEntry{ id, std::move(message) }); // 利用移动语义
}

说明:通过将字符串参数按值传递,并在插入时使用 std::move,可以避免不必要的拷贝操作,提升性能。

优化后的实现

结合上述优化措施,最终的 Logger 类如下:

#include <vector>
#include <string>
#include <mutex>
#include <thread>
#include <deque>
#include <atomic>struct LogEntry {int id;std::string message;
};class Logger {
public:Logger() {logs_.reserve(1000000); // 预先分配空间}void log(int id, std::string&& message) {auto& buffer = get_buffer();buffer.emplace_back(LogEntry{ id, std::move(message) });}std::vector<LogEntry> collect_logs() {std::vector<LogEntry> all_logs;std::lock_guard<std::mutex> lock(mutex_);for(auto& buffer : buffers_) {all_logs.insert(all_logs.end(),std::make_move_iterator(buffer.begin()),std::make_move_iterator(buffer.end()));buffer.clear();}return all_logs;}private:static const int THREAD_COUNT = 4;std::vector<LogEntry> buffers_[THREAD_COUNT];std::mutex mutex_;std::vector<LogEntry>& get_buffer() {size_t thread_id = std::hash<std::thread::id>()(std::this_thread::get_id()) % THREAD_COUNT;return buffers_[thread_id];}
};

优化效果

  • 减少内存重新分配:通过预先分配足够的空间,减少了 std::vector 的多次内存分配。
  • 降低锁争用:通过采用线程本地缓冲区,减少了多个线程对同一互斥锁的争用,提高了并行性能。
  • 消除不必要的拷贝:利用移动语义和原位构造,减少了字符串拷贝带来的性能开销。

最佳实践与总结

通过以上的讨论与实战案例,可以总结出以下 STL 性能优化的最佳实践:

  1. 优先选择 std::vector:由于其优秀的缓存局部性和较低的内存开销,std::vector 是大多数情况下的首选容器。
  2. 合理预留空间:在已知或可估算的情况下,使用 reserve 预先分配容器空间,避免多次内存分配。
  3. 使用移动语义和原位构造:尽量使用 std::moveemplace_back 等函数,减少不必要的对象拷贝。
  4. 选择合适的算法和数据结构:根据具体需求,选择时间复杂度和空间复杂度更优的算法和数据结构。
  5. 提高缓存局部性:优先选择内存连续布局的容器,提升数据访问的缓存命中率。
  6. 减少锁争用:在多线程环境下,设计数据结构和访问模式以减少同步开销,如使用线程本地存储或分离的缓冲区。
  7. 使用自定义分配器:在特定场景下,定制内存分配策略以提升内存分配效率和降低内存碎片。
  8. 持续进行性能分析与优化:通过性能分析工具不断监测和优化代码,确保优化措施实际有效。

总结:STL 是 C++ 中强大的工具,了解其性能特性并合理使用,可以显著提升项目的性能表现。通过选择合适的容器、优化内存管理、减少不必要的拷贝以及合理选择算法,开发者可以有效克服 STL 带来的性能瓶颈,实现高效、稳定的应用程序。

参考资料

  • C++ STL 官方文档
  • Effective STL
  • C++ Primer

标签

C++、STL、性能优化、编程技巧

版权声明

本文版权归作者所有,未经允许,请勿转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/76896.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型如何优化数字人的实时交互与情感表达

标题:大模型如何优化数字人的实时交互与情感表达 内容:1.摘要 随着人工智能技术的飞速发展&#xff0c;数字人在多个领域的应用愈发广泛&#xff0c;其实时交互与情感表达能力成为提升用户体验的关键因素。本文旨在探讨大模型如何优化数字人的实时交互与情感表达。通过分析大模…

qt designer 软件主题程序设计

对于使用Qt Designer设计的界面&#xff0c;主题切换的实现需要结合Qt的信号槽机制、样式表动态加载以及资源管理。以下是针对Qt Designer UI的详细解决方案&#xff1a; 一、UI文件与主题系统的整合架构 二、核心实现步骤 1. 动态样式表加载系统 // ThemeManager.h class …

一、STM32简介

一、实验器材介绍 二、STM32简介 1.STM32 名词解释 STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器。 ST&#xff0c;指ST公司&#xff08;意法半导体&#xff09;;M&#xff0c;MicroController 微控制器&#xff08;MCU,MicroController Unit 微控制器单元/单片机&…

JVM虚拟机篇(一)深入理解JVM:组成部分、运行流程及程序计数器详解

JVM虚拟机篇&#xff08;一&#xff09;深入理解JVM&#xff1a;组成部分、运行流程及程序计数器详解 JVM虚拟机篇&#xff08;一&#xff09;深入理解JVM&#xff1a;组成部分、运行流程及程序计数器详解一、引言二、JVM的组成部分2.1 类加载子系统2.2 运行时数据区2.3 执行引…

elementui的默认样式修改

今天用element ui &#xff0c;做了个消息提示&#xff0c;发现提示的位置总是在上面&#xff0c;如图&#xff1a; 可是我想让提示的位置到下面来&#xff0c;该怎么办&#xff1f; 最后还是看了官方的api 原来有个自定义样式属性 customClass 设置下就好了 js代码 css代码 效…

游戏引擎学习第204天

回顾并为今天的内容做铺垫 好&#xff0c;现在开始这一集。今天我们将进行一些用户界面编程&#xff0c;觉得这是一个展示如何编写这类代码的好时机。很多人对如何做用户界面代码都很好奇&#xff0c;所以展示一下如何编写是非常有意义的。 我之所以在现在的这个地方做这些工…

我的世界1.20.1forge模组开发进阶教程——TerraBlender

TerraBlender介绍 从模组开发者的视角来看,TerraBlender为Minecraft生物群系类模组的开发提供了全方位的技术支持,显著降低了开发门槛并提升了模组的质量与扩展性: 跨平台兼容性架构支持Forge/Fabric/Quilt/NeoForge四大主流加载器,开发者无需为不同平台单独适配代码客户端…

借助mcpo在open-webui中使用mcp

open-webui前几天发布了0.6版本&#xff0c;我立即进行了升级。新版本中一个重要功能是通过mcpo方式支持了mcp server。本文将介绍mcpo是什么&#xff0c;以及如何在open-webui中使用它。同时&#xff0c;我也会分享几个在接入过程中遇到的问题及解决方案。 首先来介绍mcpo&…

安装gpu版本的dgl

1.先去网址&#xff0c;找到对应版本的dgl,然后下载到本地。 dgl-whl下载地址 我的是python 3.8 &#xff0c;cuda 11.6. windows 2.在虚拟环境里 输入 pip install E:\dgl-1.0.2cu116-cp38-cp38-win_amd64.whl &#xff08;因为我下载到E盘里了&#xff09; 这样GPU版本的d…

PyTorch使用(7)-张量常见运算函数

1. 基本数学运算 1.1 平方根和幂运算 import torchx torch.tensor([4.0, 9.0, 16.0])# 平方根 sqrt_x torch.sqrt(x) # tensor([2., 3., 4.])# 平方 square_x torch.square(x) # tensor([16., 81., 256.])# 任意幂次 pow_x torch.pow(x, 3) # tensor([64., 729., 4096…

Nginx功能及应用全解:从负载均衡到反向代理的全面剖析

Nginx作为一款开源的高性能HTTP服务器和反向代理服务器&#xff0c;凭借其高效的资源利用率和灵活的配置方式&#xff0c;已成为互联网领域中最受欢迎的Web服务器之一。无论是作为HTTP服务器、负载均衡器&#xff0c;还是作为反向代理和缓存服务器&#xff0c;Nginx的多种功能广…

安徽京准:NTP时间同步服务器操作使用说明

安徽京准&#xff1a;NTP时间同步服务器操作使用说明 3.1 连接天线 天线连接到“ANT”口。 3.2 连接电源 将220V电源线连到AC220V座上或将电源适配器&#xff08;7.5V~12V&#xff09;接到DC口上。也可以同时接上&#xff0c;提高供电可靠性。 3.3 LAN网口 网线连接到NTP…

Java项目之基于ssm的怀旧唱片售卖系统(源码+文档)

项目简介 怀旧唱片售卖系统实现了以下功能&#xff1a; 用户信息管理&#xff1a; 用户信息新增&#xff1a;添加新用户的信息。 用户信息修改&#xff1a;对现有用户信息进行修改。 商品信息管理&#xff1a; 商品信息添加&#xff1a;增加新的商品&#xff08;唱片&#x…

基于 Python 的自然语言处理系列(70):检索增强生成(RAG)

1. 什么是 RAG&#xff1f; 在许多大模型&#xff08;LLM&#xff09;应用场景中&#xff0c;我们需要使用特定的用户数据&#xff0c;而这些数据并未包含在模型的训练集中。检索增强生成&#xff08;Retrieval Augmented Generation&#xff0c;RAG&#xff09;是一种有效的解…

CAD插件实现:所有文字显示到列表、缩放、编辑——CAD-c#二次开发

当图中有大量文字&#xff0c;需要全部显示到一个列表时并缩放到需要的文字时&#xff0c;可采用插件实现&#xff0c;效果如下&#xff1a; 附部分代码如下&#xff1a; private void BtnSelectText_Click(object sender, EventArgs e){var doc Application.DocumentManager.…

Systemd构建自动化备份服务与外部存储管理

实训背景 你是一家数据公司的系统管理员&#xff0c;需设计一套自动化备份系统&#xff0c;满足以下需求&#xff1a; 定期备份&#xff1a;每周日凌晨1点将 /data 目录压缩备份到 /backups。外部存储挂载&#xff1a;插入USB设备时自动挂载到 /mnt/usb&#xff0c;并触发增量…

PostgreSQL中根据另一表的值来更新一个字段

UPDATE table1 SET value t2.new_value FROM table2 t2 WHERE table1.id t2.reference_id; 解释 UPDATE table1&#xff1a;指定要更新的表&#xff0c;不要用别名。 SET value t2.new_value&#xff1a;设置要更新的字段及其新值&#xff0c;这里新值来自 table2。也可更…

#SVA语法滴水穿石# (000)断言基本概念和背景

一、前言 随着数字电路规模越来越大、设计越来越复杂,使得对设计的功能验证越来越重要。首先,我们要明白为什么要对设计进行验证?验证有什么作用?例如,在用FPGA进行设计时,我们并不能确保设计出来的东西没有功能上的漏洞,因此在设计后我们都会对其进行验证仿真。换句话说…

Git 从入门到精通(开源协作特别版)

&#x1f9e0; Git 从入门到精通&#xff08;开源协作特别版&#xff09; ✅ 基础命令 &#x1f9f0; 高级用法 &#x1f6e0;️ 开源实战技巧 &#x1f30d; GitHub 社区协作 适合&#xff1a;从0开始 → 熟练开发者 → 参与/维护开源项目 &#x1f530; 第1章&#xff1a;…

【SQL】取消sql某一列的唯一值key值的方法

在插入数据到sql时&#xff0c;遇到了这个问题&#xff1a; Duplicate entry ‘XXX’ for key 起因是&#xff1a; 我之前设计表的时候&#xff0c;手动给product_title 这个列加了一个key&#xff0c; key 是这个字段的唯一键约束&#xff0c;就不能重复在这一列存入重复的数…