实现一个通用的生产者消费者队列(c语言版本)

背景:笔者之前一直从事嵌入式音视频相关的开发工作,对于音视频的数据的处理,生产者消费者队列必不可少,而如何实现一个高效稳定的生产者消费者队列则十分重要,不过按照笔者从业的经验,所看到的现象,不容乐观,很多知名大厂在这种基础组件的开发能力上十分堪忧。

音视频数据处理的特点:

  • 音视频数据量大:音视频数据特别是视频数据,占据了计算机数据的很大一块,不信就看看每个人的硬盘里,去除电影,照片,mp3是不是很空荡荡的。
  • 实时性要求高:音视频的延时如果大于200ms,使用体验会十分糟糕。
  • 处理流程复杂:一帧数据从sensor捕获到最终网传输出或者lcd显示,都需要经过一系列的模块进行处理。特别是网络传输,一般要经过原始数据捕获,视频数据格式转换,数据编码压缩,数据封装打包,网络传输。

生产者消费者队列在视频数据处理的必要性:

视频数据的处理为什么需要生产者消费者队列?其实上面提到的音视频数据处理的特点就是答案。

  • 一般对视频数据处理的模块都是运行在多线程中,每一个处理模块运行在一个线程中(也是软件工程分模块的思想),相互之间通过生产者消费者队列进行数据的交互,之所以用线程模型,而没有用我一直推崇的进程模型是因为线程间的共享内存比较方便,而进程则要相对复杂的多。
  • 利用多线程/多进程的并行处理能力 ,如果采用单线程单进程的单线处理模式,一帧数据从采集到输出线性经过几个模块,时效性无法保证。
  • 缓存数据,保持平滑,通过队列缓存视频数据,可以有效的去除一些数据抖动,帮助音视频数据的平滑播放,同时这个数据的缓存又不易过多,否则加大了延时,损伤实时性,所以队列大小的设置是一个平衡的艺术。

常见的生产者消费者队列实现存在的问题:

不注重效率性能:

  • 对于buffer状态的检测采用loop轮询方式。
    loop轮询是任何有追求的程序员都要避免的处理方式,而笔者以自己经历经常看到以下类似的代码:
pthread_mutex_lock();
state = check_some_state();
if (state == xxx) {do_some_process();
} else {usleep(x);
}
pthread_mutex_unlock();

以上代码loop一个状态,如果状态成立做有效的处理,不成立则睡眠一定时间,之后再次调用该段代码进行下一次的状态检测,而这个睡眠时间是一个随机经验值,很有可能下次仍然是无效的检测,接着睡眠再loop,多余的loop是一种资源的浪费。

数据的传递采用copy方式:

数据的传递,采用copy的方式,一帧数据在一个完整的处理流程中经过n次copy(笔者见过一个系统中一帧数据copy了8次之多)

生产者消费者队列和业务代码混杂在一起,没有分离:

对于开发者,都希望用最简单的接口完成某个功能,而不关心内部实现,在这里就是,只需要生产者生产数据,消费者消费数据,而内部的处理(同步,数据的处理等)完全不关心,这样开发者就不需要去弄很多锁,降低了开发难度,也可以使代码组件化,模块化。

有经验的开发者应该感觉以上都是基础点,不会有人犯这样的错误,不过笔者以自己的经历肯定的说,以上两种问题在某世界级大厂的视频设备上随处可见。

让我悲哀的是,当我指出这些问题时,某些开发者完全无动于衷。而我更无力的是,现在的cpu,memory性能实在是高,在某些不太高端的嵌入式芯片上,优化过的数据并没有十分明显,在一个实际项目上,经过优化后cpu大概降低1%(原总系统cpu占用7%),有经验的开发者又会说原7%的cpu占用率说明这个芯片做这个系统浪费了,不过也没办法其实已经用了比较低端的芯片了。。。

以上的吐槽主要是想说明:由于cpu,memory性能的提升,让很多开发者感觉软件的优化意义不大了,而我是一个理想主义者,对于某些设计ugly的代码真的是零容忍啊。

如何优化:

以上说了这么多,那如何操作呢?

  • 提高效率,去除多余的loop轮询:
    采用线程的同步机制,当状态条件符合要求时,通过通知机制触发后续处理,这样去除了无效的loop轮询检测,linux系统下可以采用条件变量,信号量来实现,我更倾向于使用条件变量,因为它是linux系统原生支持的接口。
    提到条件变量,不得不提一个概念:同步互斥,这么一个基本的操作系统概念,我最喜欢作为面试第一题,不过能用一句话切中要害的说出之间区别和各自特点的人不是很多,答不出这题的,基本上就pass了。
  • 减少多余copy:
    采用预分配的方式,将buffer分为free和active两大类,每一类buffer又切成几个小buffer,然后通过指针将两类buffer下的小buffer链接成两个链表,使用者获取buffer通过free链表获取buffer,再将buffer put到active链表上,以上都是指针的操作,没有数据的copy,极大的减少了copy操作。(再次强调指针是个好东西)
  • 模块化组件化:
    将生产者消费者队列的处理部分完全剥离成一个独立的模块组件,对外只提供几个基本的接口,内部完成同步通知的处理。

一个简单的实现:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <time.h>#include "sfifo.h"//#define CONFIG_COND_FREE 1
#define CONFIG_COND_ACTIVE 1#define MAX_SFIFO_NUM   32struct sfifo_des_s sfifo_des[MAX_SFIFO_NUM];
struct sfifo_des_s *my_sfifo_des;struct sfifo_s* sfifo_get_free_buf(struct sfifo_des_s *sfifo_des_p)
{static long empty_count = 0;struct sfifo_s *sfifo = NULL;pthread_mutex_lock(&(sfifo_des_p->free_list.lock_mutex));
#ifdef CONFIG_COND_FREEwhile (sfifo_des_p->free_list.head == NULL) {pthread_cond_wait(&(sfifo_des_p->free_list.cond), &(sfifo_des_p->free_list.lock_mutex));}
#elseif (sfifo_des_p->free_list.head == NULL) {if (empty_count++ % 120 == 0) {printf("free list empty\n");}goto EXIT;}
#endifsfifo = sfifo_des_p->free_list.head;sfifo_des_p->free_list.head = sfifo->next;EXIT:pthread_mutex_unlock(&(sfifo_des_p->free_list.lock_mutex));return sfifo;
}int sfifo_put_free_buf(struct sfifo_s *sfifo, struct sfifo_des_s *sfifo_des_p)
{int send_cond = 0;pthread_mutex_lock(&(sfifo_des_p->free_list.lock_mutex));if (sfifo_des_p->free_list.head == NULL) {sfifo_des_p->free_list.head = sfifo;sfifo_des_p->free_list.tail = sfifo;sfifo_des_p->free_list.tail->next = NULL;send_cond = 1;} else {sfifo_des_p->free_list.tail->next = sfifo;sfifo_des_p->free_list.tail = sfifo;sfifo_des_p->free_list.tail->next = NULL;}pthread_mutex_unlock(&(sfifo_des_p->free_list.lock_mutex));
#ifdef CONFIG_COND_FREEif (send_cond) {pthread_cond_signal(&(sfifo_des_p->free_list.cond));}
#endifreturn 0;
}struct sfifo_s* sfifo_get_active_buf(struct sfifo_des_s *sfifo_des_p)
{struct sfifo_s *sfifo = NULL;pthread_mutex_lock(&(sfifo_des_p->active_list.lock_mutex));
#ifdef CONFIG_COND_ACTIVEwhile (sfifo_des_p->active_list.head == NULL) {//pthread_cond_timedwait(&(sfifo_des_p->active_list.cond), &(sfifo_des_p->active_list.lock_mutex), &outtime);pthread_cond_wait(&(sfifo_des_p->active_list.cond), &(sfifo_des_p->active_list.lock_mutex));}
#elseif (sfifo_des_p->active_list.head == NULL) {printf("active list empty\n");goto EXIT;}
#endifsfifo = sfifo_des_p->active_list.head;sfifo_des_p->active_list.head = sfifo->next;EXIT:pthread_mutex_unlock(&(sfifo_des_p->active_list.lock_mutex));return sfifo;
}int sfifo_put_active_buf(struct sfifo_s *sfifo, struct sfifo_des_s *sfifo_des_p)
{int send_cond = 0;pthread_mutex_lock(&(sfifo_des_p->active_list.lock_mutex));if (sfifo_des_p->active_list.head == NULL) {sfifo_des_p->active_list.head = sfifo;sfifo_des_p->active_list.tail = sfifo;sfifo_des_p->active_list.tail->next = NULL;send_cond = 1;} else {sfifo_des_p->active_list.tail->next = sfifo;sfifo_des_p->active_list.tail = sfifo;sfifo_des_p->active_list.tail->next = NULL;}pthread_mutex_unlock(&(sfifo_des_p->active_list.lock_mutex));
#ifdef CONFIG_COND_ACTIVEif (send_cond) {pthread_cond_signal(&(sfifo_des_p->active_list.cond));}
#endifreturn 0;
}int dump_sfifo_list(struct sfifo_list_des_s *list)
{struct sfifo_s *sfifo = NULL;sfifo = list->head;do {printf("dump : %x\n", sfifo->buffer[0]);usleep(500 * 1000);} while (sfifo->next != NULL && (sfifo = sfifo->next));return 0;
}struct sfifo_des_s *sfifo_init(int sfifo_num, int sfifo_buffer_size, int sfifo_active_max_num)
{int i = 0;struct sfifo_s *sfifo;struct sfifo_des_s *sfifo_des_p;sfifo_des_p = (struct sfifo_des_s *)malloc(sizeof(struct sfifo_des_s));sfifo_des_p->sfifos_num = sfifo_num;sfifo_des_p->sfifos_active_max_num = sfifo_active_max_num;sfifo_des_p->free_list.sfifo_num = 0;sfifo_des_p->free_list.head = NULL;sfifo_des_p->free_list.tail = NULL;pthread_mutex_init(&sfifo_des_p->free_list.lock_mutex, NULL);pthread_cond_init(&sfifo_des_p->free_list.cond, NULL);sfifo_des_p->active_list.sfifo_num = 0;sfifo_des_p->active_list.head = NULL;sfifo_des_p->active_list.tail = NULL;pthread_mutex_init(&sfifo_des_p->active_list.lock_mutex, NULL);pthread_cond_init(&sfifo_des_p->active_list.cond, NULL);for (i = 0; i < sfifo_num; i++) {sfifo = (struct sfifo_s *)malloc(sizeof(struct sfifo_s));sfifo->buffer = (unsigned char *)malloc(sfifo_buffer_size);printf("sfifo_init: %x\n", sfifo->buffer);memset(sfifo->buffer, i, sfifo_buffer_size);sfifo->size = sfifo_buffer_size;sfifo->next = NULL;sfifo_put_free_buf(sfifo, sfifo_des_p);}return sfifo_des_p;
}void *productor_thread_func(void *arg)
{struct sfifo_s *sfifo;while (1) {sfifo = sfifo_get_free_buf(my_sfifo_des);if (sfifo != NULL) {printf("+++++++++++++++++ put : %x\n", sfifo->buffer[0]);sfifo_put_active_buf(sfifo, my_sfifo_des);}//usleep(20*1000);}
}void *comsumer_thread_func(void *arg)
{struct sfifo_s *sfifo;int count = 0;while (1) {sfifo = sfifo_get_active_buf(my_sfifo_des);if (sfifo != NULL) {printf("---------------- get %x\n", sfifo->buffer[0]);sfifo_put_free_buf(sfifo, my_sfifo_des);}//usleep(10 * 1000);// if (count++ > 10000) {//  exit(-1);// }}
}int main()
{int ret;static pthread_t productor_thread;static pthread_t consumer_thread;struct sfifo_s *r_sfifo;my_sfifo_des = sfifo_init(10, 4096, 5);ret = pthread_create(&productor_thread, NULL, productor_thread_func, NULL);ret = pthread_create(&consumer_thread, NULL, comsumer_thread_func, NULL);while (1) {sleep(1);}return 0;
}

以上是一个简单的生产者消费者队列的c语言的实现,对应的头文件在本文底部(贴代码太长看起来很崩溃)。

仔细的同学可能会发现,以上代码sfifo_get_free_buf()中默认是loop轮询检测free buffer链表的,你前面不是说了一大堆不能loop吗?怎么还用loop呢?
这里其实有两个原因:

  • 生产者的loop是可以接受的,当发生多余loop,无法命中时,说明生产者太快,消费者太慢,而其实对于一个生产者消费者模型出现以上问题时,说明整个业务流程要重新考虑,因为正常的情况是消费者总是要快于生产者,这个业务模型才能正常的运行下去。
  • 对于有些业务模型,生产者业务模块部分是不能阻塞的,也就是说,如果free list没有数据,我们采用pthread_cond_wait()阻塞后,会导致生产者出现问题,这样最好的处理方式就是生产者模块接口返回出错,生产者业务方丢弃数据(此时就是丢帧了,这种情况如果频繁发生是不能接受了,不过也说明了消费者要有足够的能力处理生产者生产出的数据,否则整个业务都是有问题)

这个实现有那些优势:

走读和运行以上代码的同学应该可以发现这里做了一个简单可运行的demo模拟了生产者和消费者双方:

void *productor_thread_func(void *arg)
{struct sfifo_s *sfifo;while (1) {sfifo = sfifo_get_free_buf(my_sfifo_des);if (sfifo != NULL) {printf("+++++++++++++++++ put : %x\n", sfifo->buffer[0]);sfifo_put_active_buf(sfifo, my_sfifo_des);}//usleep(20*1000);}
}void *comsumer_thread_func(void *arg)
{struct sfifo_s *sfifo;int count = 0;while (1) {sfifo = sfifo_get_active_buf(my_sfifo_des);if (sfifo != NULL) {printf("---------------- get %x\n", sfifo->buffer[0]);sfifo_put_free_buf(sfifo, my_sfifo_des);}//usleep(10 * 1000);// if (count++ > 10000) {//  exit(-1);// }}
}

这里面对于使用者的优点有:

  1. 接口简单:只需要get free,put active;get active,put free。
  2. 没有了数据copy,只需要操作链表上的buffer就可以了,而这些buffer的参数控制通过init接口设置。
  3. 不用再控制sleep的时间值:前面提到,在loop模型下,如果状态不成立需要sleep一段时间,再次检查,这样来控制同步状态,而这个时间值很难确定,如果时间值过长,则会导致状态检测不及时,延误数据处理,如果时间值太短,则会增加状态检测miss cache的次数,耗费更多cpu资源。而采用本模块的实现则完全不需要考虑这些问题,只需要衔接业务处理,sleep,同步,yield cpu的操作都由这个模块实现吧,完全不需要关心。
  4. 模块化,完全和业务处理无关,可以毫无压力的运用在不同的业务处理逻辑中,没有剥离代码的工作。

以上描述了一个生产者消费者队列c语言的实现,为什么是c语言版本的?因为其他高级语言,有很多成熟的库提供了该功能,完全不用自己写,而c就没这么完善了,不过这也说明了c的简单灵活。但悲哀的是很多人因此进行了很ugly的实现。
多吐槽几句,嵌入式行业由于各种技术原因,导致开发语言还是采用c,这样对开发人员有了不小的要求,而如何才能写一些优雅的代码,对人的素质有了要求,但现状是优秀的开发者都被互联网行业抢走了,导致嵌入式行业开发人员的水平参差不齐,本来应该是一个对编码能力要求很高的行业被一些水平低下的开发者占据。so,我离开了这个行业了。。。

附:模块头文件,类linux用户可通过gcc xxx.c命令build该demo,然后运行测试。

#ifndef SFIFO_H_
#define SFIFO_H_struct sfifo_list_des_s {int sfifo_num;struct sfifo_s *head;struct sfifo_s *tail;pthread_mutex_t lock_mutex;pthread_cond_t cond;
};struct sfifo_des_s {int sfifo_init;unsigned int sfifos_num;unsigned int sfifos_active_max_num;struct sfifo_list_des_s free_list;struct sfifo_list_des_s active_list;
};struct sfifo_s {unsigned char *buffer;unsigned int size;struct sfifo_s *next;
};extern struct sfifo_des_s *sfifo_init(int sfifo_num, int sfifo_buffer_size, int sfifo_active_max_num);/* productor */
extern struct sfifo_s* sfifo_get_free_buf(struct sfifo_des_s *sfifo_des_p);
extern int sfifo_put_free_buf(struct sfifo_s *sfifo, struct sfifo_des_s *sfifo_des_p);/* consumer */
extern struct sfifo_s* sfifo_get_active_buf(struct sfifo_des_s *sfifo_des_p);
extern int sfifo_put_active_buf(struct sfifo_s *sfifo, struct sfifo_des_s *sfifo_des_p);#endif

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

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

相关文章

美媒:小米新浪达成合作 采取行动对抗腾讯

来自美媒的报道称&#xff0c;两家中国最具发展潜力的科技公司&#xff0c;新浪和小米将会共同合作&#xff0c;结合各自的通信应用程序来共同对抗移动通信的挑战&#xff0c;尤其是拥有2亿用户的强劲对手微信。 来自中国的消息称&#xff0c;这次新浪与小米的合作将会涉及到新…

Linux expr命令、Linux wc命令、Linux let 命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 expr命令是一个手工命令行计数器&#xff0c;用于在UNIX/LINUX下求表达式变量的值&#xff0c;一般用于整数值&#xff0c;也可用于字符…

【English】六、am,is,are 分别用在什么地方

is&#xff1a;第三人称单数am&#xff1a;第一人称单数are&#xff1a;第二人称单数&#xff0c;第一、二、三人称的复数 用于第一人称, I am ......(我是.......)用于第三人称, He is ......(他是......) 或She is ......(她是......), It is ......(它是.......)用于第二人…

误删了公司数据库,但我还是活下来了

专栏 | 九章算法 网址 | www.jiuzhang.com 上周我与同事们进行了一次关于职业生涯中搞砸了一些事情的简短谈话。这确实会沦为他人笑柄&#xff0c;却更给我们带来了珍贵的教训。重要的是&#xff0c;我们应该分享那些曾经的错误&#xff0c;这样其他人就可以从其中学习。下文是…

改良程序的11技巧

有很多理由都能说明为什么我们应该写出清晰、可读性好的程序。最重要的一点&#xff0c;程序你只写一次&#xff0c;但以后会无数次的阅读。当你第二天回头来看你的代码时&#xff0c;你就要开始阅读它了。当你把代码拿给其他人看时&#xff0c;他必须阅读你的代码。因此&#…

历时四年,给Google提交的Android Framework Bug终于被Fixed了

历时四年&#xff0c;Google终于修复了一个我发现的Android Framework Bug 2014年在做一个Android终端设备开发过程中&#xff0c;发现了一个Android Framework层的Bug&#xff0c;给Google提交了issue和解决方案&#xff0c;和外界传言一致Google一般不太在意个人开发者提交的…

Linux ping命令、Linux kill命令、Linux logname命令、 Linux logout命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Linux ping命令用于检测主机。 执行ping指令会使用ICMP传输协议&#xff0c;发出要求回应的信息&#xff0c;若远端主机的网络功能没有…

前端布局神器display:flex

2009年&#xff0c;W3C提出了一种新的方案--Flex布局&#xff0c;可以简便、完整、响应式地实现各种页面布局。目前已得到所有现在浏览器的支持。 flex浏览器支持一、Flex布局是什么&#xff1f; Flex是Flexible Box的缩写&#xff0c;翻译成中文就是“弹性盒子”&#xff0c;用…

bind简单转发实验

2019独角兽企业重金招聘Python工程师标准>>> *主配置文件内容// [rootlocalhost /]# cat /etc/named.conf // // named.conf // // Provided by Red Hat bind package to configure the ISC BIND named(8) DNS // server as a caching only nameserver (as a local…

数据结构:块状链表

一、概述 有时候我们需要设计这样一种数据结构&#xff1a;它能快速在要求位置插入或者删除一段数据。先考虑两种简单的数据结构&#xff1a;数组和链表。数组的优点是能够在O(1)的时间内找到所要执行操作的位置&#xff0c;但其缺点是无论是插入或删除都要移动之后的所有数据&…

记账本开发小计(四)

今天处理的是记账本小软件中的查询功能&#xff0c;由于账目的要求就是准确性&#xff0c;所以对于记账本程序来说&#xff0c;模糊查询并不适用&#xff0c;所以在这里只能是按照指定的条件来进行查询所以我做的事按照时间进行查询&#xff0c;为了方便进行处理&#xff0c;这…

Linux ps命令、Linux top命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Linux ps命令用于显示当前进程 (process) 的状态。 语法 ps [options] [--help][options] [--help] 参数&#xff1a; ps 的参数非常…

Prime Distance POJ - 2689 线性筛

一个数 $n$ 必有一个不超过 $\sqrt n$ 的质因子。 打表处理出 $1$ 到 $\sqrt n$ 的质因子后去筛掉属于 $L$ 到 $R$ 区间的素数即可。 Code: #include<cstdio> #include<cstring> #include<algorithm> #include<iostream> using namespace std; const…

给定a和n,计算a+aa+aaa+a...a(n个a)的和(大数据处理)

题目描述&#xff1a;给定a和n&#xff0c;计算aaaaaaa...a(n个a)的和。 输入&#xff1a;测试数据有多组&#xff0c;输入a&#xff0c;n&#xff08;1<a<9,1<n<100&#xff09;。 输出&#xff1a;对于每组输入,请输出结果。 样例输入&#xff1a;1 10 样例输出&…

ssh和rsh的区别、Linux rsh命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 ssh 和 rsh的区别主要有: 1 安全级别不同, 主要是ssh的密码等都是加密传输,而且还有密钥认证的机制, rsh明文传输. 而且没有密钥的机制.…

Java并发编程(多线程)中的相关概念

众所周知&#xff0c;在Java的知识体系中&#xff0c;并发编程是非常重要的一环&#xff0c;也是面试中必问的题&#xff0c;一个好的Java程序员是必须对并发编程这块有所了解的。 并发必须知道的概念 在深入学习并发编程之前&#xff0c;我们需要了解几个基本的概念。 同步和异…

4、容器虚拟化网络概述

Docker 网络 Docker 的网络实现其实就是利用了 Linux 上的网络名称空间和虚拟网络设备&#xff08;特别是 veth pair&#xff09;。 Linux 网络命名空间&#xff1a;https://www.jianshu.com/p/369e50201bce Linux虚拟网络设备之veth&#xff1a; https://segmentfault.com/a/1…

Linux whoami命令、Linux su命令、Linux w命令

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Linux whoami命令用于显示自身用户名称。 显示自身的用户名称&#xff0c;本指令相当于执行"id -un"指令。 语法 whoami […

Weekly 10

Algorithm 1.Remove Element What 移除数组中的指定元素,返回处理后的长度sum,并且数组前sum长度的元素为处理后的元素,不用额外数组&#xff0c;O(1)。How 用快慢指针,快指针遍历,遇到不等于指定元素的替换掉慢指针,然后慢指针前进一位即可。Key Codesclass Solution {public …

大数据计算:如何仅用1.5KB内存为十亿对象计数

摘要&#xff1a;AddThis的数据分析副总监Matt Abrams在High Scalability上发表了一篇文章&#xff0c;介绍了他们公司如何应对大数据。Matt Abrams表示&#xff0c;AddThis仅仅用了1.5KB内存的内存就计算了十亿个不同的对象&#xff0c;这与他们所使用的计算方法分不开的。 A…