`
haiyang
  • 浏览: 70968 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
社区版块
存档分类
最新评论

Rails 常见性能问题一览(转)

阅读更多

Rails 常见性能问题一览

作者 Stefan Kaes译者 Jason Lai 发布于 2007年3月26日 上午6时48分

社区
Ruby
主题
Web框架,
性能和扩展性,
Ruby on Rails

在 最近几个月里,我从性能问题的角度,分析了不少 Rails 应用程序(里面有一些牵涉到我的咨询业务,另外一些则是开源应用)。这些应用程序面向的多个领域之间存在着诸多差异,导致每项性能调优任务都颇具挑战性。 然而,它们之间还是存在不少共性,使得我们得以提炼出不少出现问题的地方,正是它们导致了很多应用程序难以达到高性能。它们包括:

  • 选用了缓慢的 Session 容器
  • 本可在启动时一次性完成的操作被放在每次请求中执行
  • 请求处理过程中重复相同运算
  • 频繁从数据库加载过多数据(尤其在使用关联进行连接的情况下)
  • 过分依赖于低效 helper 方法

除此之外,Rails 框架自身依然存在某些问题区(problem areas),它们的性能是我所希望在今后改善的。里面有一部分可以在应用程序级别解决,另外一部分则无能为力。下面是我比较感兴趣的问题:

  • Route 识别和 Route生成
  • ActiveRecord 对象的构造
  • SQL 查询的构造

我会在下文中罗列出针对以上问题的部分编码技巧。

一些忠告

遵循本文的建议,有可能改善你的应用程序的性能,但也可能行不通。性能调优是件费脑筋的事情,尤其在实现语言结构的性能特征没有被明确规范的时候,更是如此(在 Ruby 里面就是这种情况)。

强烈建议,在进行任何变更之前,先评测一下你的应用程序原先的性能,之后每次变更后再次进行评测。我编写了一个包,叫做 railsbench,这是一个很不错的性能回归测试工具。它简单易用,只需几分钟你就可以得到性能的基准数据。不过比较遗憾,它无法告诉你,在你的应用程序里时间到底花费在哪里(译注:此外,你的应用程序所调用的 Rails 框架的代码也是有时间开销的)

假如你手头有台运行 Windows 的机器(或者一台带双启动的 Intel Mac),我建议你去试试 Software Verification Ltd.(SVL)的 Ruby Performance Validator(RPVL)。对于我深及 Rails 框架内核的 Rails 性能调优工作,特别是在我所建议的在 已有热点视图基础上的调用图功能被 SVL 实现之后,我发现它真是功勋卓著。据我所知,它是目前市面上唯一的 Ruby 应用程序性能分析工具。Railsbench 内建了对 RPVL 的支持,这样也使得在 RPVL 之下运行被 Railsbench 定义的基准变得易如反掌。

选用 Session 容器

Rails 提供了几个内建的 Session 容器。在所有我分析过的应用程序里,要么使用了将 Session 信息储存在你文件系统上独立文件的 PStore,要么用了和数据库打交道的 ActiveRecordStore。这两个方案都不甚理想,特别是拖累了缓存 Action 的页面(action cached pages)。这里提供两个好用得多的备选方案供大家参考:SQLSessionStoreMemCacheStore

SQLSessionStore 通过以下手段避免了 ActiveRecordStore 相关的额外性能开支:

  • 避免使用事务(对于 SQLSessionStore 的正确操作,它们并不是必要的)
  • 撤销向数据库更新“created_at”和“updated_at”的操作

如果使用 MySQL,你应当保证使用 MyISAM 表来存储 Session 数据,因为它要比 InnoDB 表快很多, 并且它不会强制你使用事务处理。前不久我又为 SQLSessionStore 增加了 Postgres 支持,不过,用于 Session 存储 Postgres 看起来要比 MySQL 慢得多。因此,如果你打算使用基于数据库的 Session 存储,我推荐为 Session 表安装 MySQL 数据库(我想不出一个基于 session id 的需要连接的更好用例(use case))。

MemCacheStore 要比 SQLSessionStore快得更多。我的测评结果显示,对于缓存了 Action 的页面它能够带来了 30% 的速度提升 。你得先安装 Eric Hodel 的 memcache client ,并在 environment.rb 中做相应配置之后才能正常使用。注意:Ruby-Memcache 还是不要去试的好(实在、实在慢得让人难以忍受)。

在我自己的项目中,我更倾向于使用基于数据库的 Session 存储,原因是可以使用 Rails 命令行或者数据库软件包提供的管理工具进行简单得管理。对于 MemCacheStore 你就得自己为它编写脚本了。另一方面,对于高访问量网站来说,内存缓存方式的扩展性更好一些,并且它随支持 Session 超时(session expiry)的 Rails 一起提供。

在请求过程中缓存运算结果

假如你在处理一次请求过程中不止一次需要某些相同的数据,但由于你的数据以某种形式依赖于请求参数,而不能使用类级别缓存的时候,那么请将数据缓存起来,以避免重复计算。

要采取这种模式很简单:

module M
def get_data_for(request)
@cached_data_for_request ||=
begin
expensive computation depending on request returning data
end
end
end

你的代码可以是简单的  "A".."Z".to_a ,或者是一个数据库查询,例如取得一个指定的用户等。

在启动或者首次访问时执行与请求无关的运算

这个性能忠告太简单了,我都有点儿犹豫是不是还该把它放在本文中谈论——然而,我还是发现很多我分析过的应用程序并没有采取这项有效的优化技术。

事实上,这个技术再简单不过了:如果你的应用程序里面存在哪些数据,在整个生命周期里都不会发生改变,或者数据改变之少到发生变更之后进行一次服务器重启就可以解决问题的话,那么,请把这些数据缓存在你的应用程序中某个类适当的类变量中。常见的范例如下:

class C
@@cached_data = nil
def self.cached_data
@@cached_data ||= expensive computation returning data
end
...
end

实际的例子有:

  • 应用程序的配置数据(如果你把应用程序设计成其他人可以安装的方式)
  • 数据库中恒定不变的(静态的)数据(否则使用缓存)
  • 使用 ObjectSpace.each 检测已安装的 Ruby 类/模块

优化查询

Rails 提供了功能强大的领域特定语言(Domain Specific Language),用来定义模型类之间反映数据表关系的关联。哎,可惜目前的实现仍然没有对性能进行优化。依赖于内建生成的访问器会严重地影响性能。p>

首当其冲的问题就是常被大家称作“1 + N”查询的问题:如果你从类 Article(数据表“articles”)加载 N 个对象,而类 Article 到类 Author(数据表“authors”)之间存在多对一的关系,使用生成访问器方法访问一个特定的 Article 将会触发 N 个额外的数据库查询。这自然而然就给数据库带来了额外负担,不过对于 Rails 应用服务器性能来说,更重要的是,要提交的 SQL 查询语句会为已经访问过的对象被重新进行构造。

要解决这个额外开销,你可以像下面一样在你的查询参数中添加一个 :include => :author:

Articles.find(:all, :conditions => ..., :include => :author

这样一来,通过提交单独的 SQL 语句以及立即构造 Author 对象的方式,上述额外开销都能得以避免。这项技术常常被称为“贪婪关联查找(find with eager associations)”,并且它对于其它关系类型同样适用(比如一对一、一对多或者多对多关联)。p>

尽管如此,我们还可以使用一项叫做“携带加载(piggy backing)”的技术来进一步优化多对一关系:来自原数据表的属性连同连接表的属性均由带表连接的 ActiveRecord 对象。因此,一条带连接的查询就可以从数据库全数抓取所需的信息。假设你的视图要显示的只是关联到文章信息的作者名称,你可以把上面的查询替换成:

Articles.find(:all, :conditions => ...,
:joins => "LEFT JOIN authors ON articles.author_id=authors.id",
:select => "articles.*, authors.name AS author_name")

此外,假如你的视图显示的是可用的文章字段的一个子集,譬如“title”、“author_id”和“created_at”,那么你得把上述代码改成:

Articles.find(:all, :conditions => ...,
:joins => "LEFT JOIN authors ON articles.author_id=authors.id",
:select => "articles.id, articles.title, articles.created_at, articles.author_id, authors.name AS author_name")

一般说来,仅加载部分对象可以在一定程度上提升查询的速度,尤其在你的模型对象拥有大量字段的时候更是如此。为了使用这项技术达到全面提速,你还得在模型类中定义一个方法,用来访问查询附带的所有属性:

class Articles
...
def author_name
@attributes['author_name'] ||= author.name
end
end

当你编写视图代码时,使用这个模式,你就可以不必去了解原来的查询是否存在连接了。

假如你的数据库支持视图,你可以定义一个仅包含必要信息的视图,这样就不必手动编写复杂的查询代码了。这么做的另外一条好处是,可以为你从连接表内加载的字段获得正确的数据类型转换。到目前为止,Rails 尚未提供这些功能,你必须手动为它们编写代码。

给“潮流前沿的伙计们”的一点补充:翻来覆去捣腾这些相同的模式真是让我颇为厌倦,所以我编写了一些扩展代码用来自动完成大部分这类任务。请到我的博客上查看其预发行版

避免使用笨拙迟缓的 helper 方法

在 Rails 内核中,有许多 helper 方法运行缓慢。一般而言,所有取 URL hash 作参数的 helper 方法都会调用 routing 模块,来生成引用前基础控制器 action 的最短 URL。这就意味着 route 文件中的几个 routes 应该进行检查,这个过程通常煞费时间。哪怕只是使用像下面一个简单的 route 文件:

ActionController::Routing::Routes.draw do |map|
map.connect '', :controller => "welcome"
map.connect ':controller/service.wsdl', :action => 'wsdl'
map.connect ':controller/:action/:id'
end

你将会发现写成

link_to "Look here for joke #{h @joke.title}",
{ :controller => "jokes", :action => "show", :id => @joke },
{ :class => "joke_link" }

和直接编写这样精简的HTML之间显著的性能差异:

<a href="/jokes/show/<%= @joke.id %>"
class="joke_link">Look here for joke <%= h @joke.title %></a>

对于显示大量链接的页面,我所测得的速度提升比例最高能达到 200%(前提是其它部分已经进行了优化)。

为使模板代码可读性更强,并避免不必要的重复,我通常在 application.rb 内加入生成链接的 helper 方法:

def fast_link(text, link, html_options='')
%(<a href="#{request.relative_url_root}/#{link}"> hmtl_options>#{text})
end

def joke_link(text, link)
fast_link(text, "joke/#{link}", 'class="joke_link"')
end

将上述范例改成:

joke_link "Look here for joke #{h @joke.title}", "show/<!---->"

用这种方式来解决 route 生成缓慢的问题显然过于繁琐,一般只有性能要求很高的页面才应当使用。如果你不必马上进行性能调优的话(啊呃,读起来挺像我每天收到的垃圾邮件的感觉), 不妨关注我那即将问世的模板优化工具的第一个发行版本,它将在大多数情况下自动帮你完成所有此类工作。

未来 Rails 性能调优的话题

正如上文所说,route 识别和 route 生成的性能需要有些东西来协助解决。我的模板优化工具将解决 route 生成问题。上周又有一个新的 route 实现被整合进 Rails 开发分支中。我进行了一些评测,结果显示,在 route 识别方面,性能似乎得到了一些改善。但就 route 生成而言,结果则比较参差不齐。新的实现是否能改善到一直都比先前版本的速度快,尚且有待观望。

从数据库加载大量 ActiveRecord 对象相对来说是比较慢的,当然程度并不高,因为实际的数据库连接传输开销比较大。不过,由于行数据被表示成用字符串作为键值索引的 hash,在 Rails 内部构造 Ruby 对象的开销相当昂贵。转而使用基于数组的数据行类(array based row class)可以改善这个问题。然而,要做得恰如其分的话,就得牵扯到 ActiveRecord 实现的核心部分,因而我们只能寄希望它在 Rails 2.0 中推出。

最后一点,目前 SQL 查询的构造方式,使得生成查询语句的运算比从数据库获取实际数据的代价更为高昂。要大幅度改善这个现状,我的看法是,可以使绝大多数 SQL 查询只生成一次,然后代入实际参数值进行扩充。当前唯一能解决这个问题的方法只有手动编写你的查询代码。

注:以上关于追求更优性能的可行方案,是我个人的观点,并不代表任何官方“核心团队”对待这些问题的看法。

结语

上文列举的问题并不想给你一种 Rails 可能运行缓慢(乃至于我觉得它太过迟缓)的心里暗示。恰恰相反,我坚信 Rails 是一个卓越的 Web 应用开发框架,对于稳健而又快速的 Web 应用的开发大有裨益,并且带来开发效率的改善。正如所有框架一样,它提供了方便的使用方法,可以大幅度地提升你的开发速度,而且在绝大多数场合下适用于你 绝大部分的需求。不过,有些时候,你必须尽可能在每一秒内腾出一些额外的请求,或者你在硬件资源上面受到一定限制,了解如何进行性能调优是大有帮助的。一 旦你碰上性能问题,希望本文帮助你理清某些范围的要点,助你一臂之力。

关于作者

Stefan Kaes 是 Rails 性能方面的权威博客RailsExperess 和于 2007 年初发行的《Performance Rails》一书的作者。Stefan 的书将跻身于 Addison-Wesley 新系列丛书 Professional Ruby 的首发阵容。此系列丛书将于 2006 年底面世,包括 Hal Fulton 的《The Ruby Way》第二版,以及丛书编辑 Obie Fernandez 所著的旗舰大作《Professional Ruby on Rails》。该系列将是一套精辟入里的学习工具型丛书,从专业的高度帮助读者最大限度从 Ruby 和 Rails 中汲取精华。

<script type="text/javascript"> var replyEnabled=true; var forumID=1; var threadID=1504; var previewText='预览'; var pleaseWait='请稍候……'; var reply='回复'; var postMessage='发送消息'; var errorSubject='请输入主题。'; var errorBody='您不允许发表无内容的消息。请输入您的消息并重试。'; var cancel='取消'; var goBackOrEdit='返回/编辑'; var re='Re:'; var lastMessage=0; var stopWatchText='取消对此讨论的关注'; var startWatchText='关注此讨论'; var descending='false'; var ctxPath= ''; var postAddress= ctxPath + '/forum/post!post.action?language=' + 'zh'; var postAddWatches= ctxPath + '/forum/watches!add.action'; var postRemoveWatches= ctxPath + '/forum/watches!remove.action'; var loggedIn=false; </script> <script type="text/javascript" src="http://www.infoq.com/scripts/forum.js;jsessionid=6DAB093AB0EF40F5603F33BDC0028F03"></script>

2 条回复

回复

fast_link 好像有错误 发表人 Suave Su 发表于 2007年5月31日 下午9时56分 <script language="javascript" type="text/javascript"> Event.observe('tooltip_7139', 'mouseover', initializeTooltip); Event.observe('tooltip_7139', 'mouseout', deintializeTooltip); </script>
Re: fast_link 好像有错误 发表人 Suave Su 发表于 2007年5月31日 下午9时57分 <script language="javascript" type="text/javascript"> Event.observe('tooltip_7140', 'mouseover', initializeTooltip); Event.observe('tooltip_7140', 'mouseout', deintializeTooltip); </script>
  1. 返回顶部

    fast_link 好像有错误

    2007年5月31日 下午9时56分 发表人 Suave Su

    我现在的写法是:
    def fast_link(text, link, options='')
    %(#{text})
    end

  2. 返回顶部

    Re: fast_link 好像有错误

    2007年5月31日 下午9时57分 发表人 Suave Su

    code帖不进去,被parse了 -___-

分享到:
评论

相关推荐

    Play_Framework_框架教程.pdf

    在性能方面,Play Framework有优化的性能比较数据,且其生成的war包和所用到的jar包一览是开发人员在部署时需要关注的要点。Play能够直接生成war包,方便部署到不同的应用服务器上。 Ajax技术在Play中的使用也是其...

    网站后台管理模板 网站后台管理模板

    此外,采用现代前端框架(如React、Vue或Angular)和后端技术(如Node.js、Django或Ruby on Rails)可以提高性能和开发效率。 总之,网站后台管理模板是构建高效、易用的后台管理系统的关键。它应该包含一套完整的...

    文化遗产保护:MATLAB点云处理在古建筑形变监测中的实践.pdf

    文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 你是否渴望高效解决复杂的数学计算、数据分析难题?MATLAB 就是你的得力助手!作为一款强大的技术计算软件,MATLAB 集数值分析、矩阵运算、信号处理等多功能于一身,广泛应用于工程、科学研究等众多领域。 其简洁直观的编程环境,让代码编写如同行云流水。丰富的函数库和工具箱,为你节省大量时间和精力。无论是新手入门,还是资深专家,都能借助 MATLAB 挖掘数据背后的价值,创新科技成果。别再犹豫,拥抱 MATLAB,开启你的科技探索之旅!

    敏感图片敏感图片敏感图片敏感图片敏感图片敏感图片11

    敏感图片敏感图片敏感图片敏感图片敏感图片敏感图片11敏感图片敏感图片敏感图片敏感图片敏感图片敏感图片1122

    通信管道统建工程施工合同.docx

    通信管道统建工程施工合同.docx

    挂载网盘到本地工具.zip

    挂载网盘到本地工具.zip

    Spatial intelligence and geography 空间智能和地理.doc

    Spatial intelligence and geography 空间智能和地理.doc

    黑板风格毕业答辩模板25个

    黑板风格毕业答辩模板是一系列富有创意和趣味性的答辩文档模板,专为追求独特表达的大学生设计。这25个模板模拟了传统黑板的效果,结合了手绘风格与现代设计理念,使得内容呈现既生动又具学术感。每个模板都强调清晰的结构和易于理解的布局,适用于各类学科和研究领域,帮助学生有效地展示研究成果和核心观点。 黑板风格不仅带来亲切感,还能唤起人们对课堂学习的回忆,为答辩增添了轻松而专业的氛围。这些模板配备了丰富的图标、示意图和配色,既美观又实用,能够帮助学生在答辩中更好地吸引评审的注意力,增强信息的传达效果。无论是科技、艺术还是人文社科,黑板风格毕业答辩模板都能够为你的演示增添一份独特的魅力,提升你的表现,助力你在毕业答辩中取得成功。

    世邦魏理仕:2021年重庆房地产市场回顾与2022年展望.pdf

    世邦魏理仕:2021年重庆房地产市场回顾与2022年展望

    IMG_20250416_154916.jpg

    IMG_20250416_154916.jpg

    智慧农业大数据平台解决方案PPT(65页).pptx

    智慧农业,作为现代农业的新篇章,正引领着农业生产的革命性变革。本解决方案以物联网、云计算、大数据等先进技术为核心,为农业生产打造了一套全面、智能的管理系统。 想象一下,从温室大棚到广袤田野,智能传感器遍布每个角落,它们能实时感知空气温湿度、土壤水分、光照强度等环境参数,仿佛为农作物装上了“眼睛”和“耳朵”。这些数据通过物联网技术传输到云端,经过大数据分析,为农民提供精准的种植建议,如何时灌溉、施肥、防虫,让农业生产变得更加科学、高效。 更有趣的是,通过智慧农业平台,农民可以远程监控作物生长情况,甚至用手机就能控制温室大棚的遮阳板、通风设备等,实现“指尖上的农业”。此外,方案还包含了农产品可追溯系统,从田间到餐桌,每一步都可追溯,让消费者吃得放心。而智慧农业电商平台,则让农产品销售更加便捷,农民直接对接市场,收益倍增。 总之,这套智慧农业解决方案不仅让农业生产变得更加智能、高效,还提升了农产品的质量和安全,为农民带来了实实在在的收益,开启了农业现代化的新篇章。 对于想要投身智慧农业领域的你来说,这不仅仅是一套解决方案,更是一把开启现代农业大门的钥匙,引领你走向更加辉煌的未来。

    自动承保新员工示例-雇主责任险

    自动承保新员工示例-雇主责任险

    Java编程实现Redis集群连接与操作:含密码配置及异常处理机制设计了文档的主要内容

    内容概要:本文档主要介绍了Java代码连接Redis集群的方法,包括早期尝试的代码和修正后的有效代码。早期代码使用了`ShardedJedisPool`来管理多个`JedisShardInfo`对象,并设置了连接池参数和密码认证。然而,这段代码在实际的Redis集群环境中无法正常运行。修正后的代码采用了`JedisCluster`类,并通过配置连接池参数、设置密码以及定义集群节点信息,实现了与Redis集群的成功连接。此外,还提供了获取哈希表数据和集合数据的示例方法。 适合人群:具备一定Java编程经验,尤其是对Redis有一定了解的研发人员。 使用场景及目标:①学习如何正确配置Java代码连接Redis集群,包括设置连接池参数、密码认证和集群节点信息;②掌握通过`JedisCluster`类进行基本的Redis操作,如获取哈希表数据和集合数据。 阅读建议:读者应重点关注修正后的代码部分,理解其相对于早期代码的改进之处,特别是关于`JedisCluster`类的使用方法及其在连接Redis集群中的优势。同时,在实践中可以根据自己的需求调整连接池参数和密码配置。

    通信基站几种供电方案比较.doc

    通信基站几种供电方案比较.doc

    MCP协议从原理到开发保姆级教程.pdf

    内容概要:本文详细介绍了MCP(Model Context Protocol,模型上下文协议)的原理与开发流程。MCP协议起源于2024年11月25日Anthropic发布的文章,旨在解决不同AI模型间工具调用的碎片化问题,提供标准化接入方式,实现多功能应用与创新体验。文章首先解释了MCP的核心概念及其重要性,接着深入探讨了MCP Server和Client的开发步骤,包括项目搭建、工具注册、资源创建、提示符配置以及调试方法。此外,还讲解了MCP的工作原理,特别是C/S架构、JSON-RPC 2.0协议的应用,以及模型如何选择和执行工具。最后,通过一个实际案例展示了如何利用MCP协议构建一个企业微信机器人,实现查询知识库并将结果发送到群聊中。 适合人群:具备一定编程基础,对AI模型集成、标准化开发感兴趣的开发者,尤其是从事大语言模型相关工作的工程师。 使用场景及目标:①解决多模型集成中的工具调用标准化问题;②构建具备多种功能的企业级AI应用,如邮件发送、图像生成等;③学习如何通过MCP协议实现模型决策与工具执行的解耦,提升开发效率和用户体验。 阅读建议:本文适合有一定编程经验的读者,尤其是对AI模型集成有需求的技术人员。建议读者跟随文中提供的示例代码进行实践,理解MCP协议的核心机制,并尝试构建自己的MCP应用。同时,关注官方文档和社区动态,以获取最新的技术支持和开发指南。

    bofangBEEP.zip

    bofangBEEP

    【微电网优化】基于matlab改进的粒子群算法求解含风光柴储的微电网模型问题【含Matlab源码 13169期】.zip

    Matlab领域上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作

    Qt+Opencv 显示多摄像头画面

    Qt+Opencv 显示多摄像头画面

    金融领域中国房地产行业2023年信用展望:政策支持与债务压力下的市场前景分析

    文章基于彭博专业服务提供的数据,对中国房地产行业2023年的信用展望进行了详细分析。首先,中国政府出台了16项措施支持房地产行业发展,旨在打破行业恶性循环并改善地产商的现金流状况。政府的支持措施包括银行贷款、信托展期、债券发行计划等,这些措施有助于缓解地产商的资金压力,但效果取决于贷款规模。其次,文章指出,若政府持续提供支持,房地产销售有望复苏,尤其是投资级地产商的现金覆盖率较高,能更好地应对债务到期压力。然而,高收益地产商的现金覆盖率较低,面临更大的再融资风险。此外,2023年中资地产债到期规模较大,尤其是在第一季度和第二季度,可能导致再融资缺口扩大。最后,文章分析了不同评级地产商的债券表现,指出投资级地产债可能面临与高收益地产债类似的再融资压力,而高收益地产债的价格已接近历史低点,进一步下行空间有限。

    第十一章:链表和共用体的个别例子

    第十一章:链表和共用体的个别例子,第十一章:链表和共用体的个别例子,第十一章:链表和共用体的个别例子

Global site tag (gtag.js) - Google Analytics