内联函数真的可以提高程序执行效率吗

内联函数真的可以提高程序执行效率吗,第1张

是的,内联函数省去了函数调用的开销(比如通常的函数通过stack传递参数都需要调用指令)

但相应的,由于每次调用inline function都会重复代码,所以会造成生成的执行文件稍大。。。

使用xdebug扩展。

关键配置文件

[xdebug]

;基本调试配置

xdebugauto_trace = onxdebugcollect_params = onxdebugcollect_return = onxdebugprofiler_enable = onxdebugprofiler_output_dir ="/php/ext/xdebug_profilers"xdebugtrace_output_dir = "/tmp/ext/xdebug_traces";远程调试设置

xdebugremote_enable = onxdebugremote_host = localhost

xdebugremote_port = 9010xdebugremote_autostart = on

 zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20131226/xdebugso

扩展目录自己改一下

然后重新web服务。

访问PHP脚本。此时在用户主目录下会有cachegrindoutxxx的分析文件:

该文件的内容不是很直观,所以需要使用可视化的工具来查看和分析,而Xdebug本身就支持使用第三方的可视化profiler文件的内容。在Linux下,可以使用KCacheGrind,而在Windows平台,可以使用QCacheGrind,当然还有一些在线的由爱好者开发的工具,例如:WebGrind

   技巧 晚点获取资源 早点释放资源 这是个小技巧 通常 最好晚点获取资源而要早点释放资源 这些资源包括 对象 文件句柄和其他资源   ADO 连接和记录集是这种优化的首要目标 当您使用完记录集 就是说用它的数据打印完一个表格后 请立即将它释放 而不是等到页的末尾 将您的 VBScript 变量设置为  Nothing  是最好的做法 不要让记录集简单地脱离作用域 同时 应当释放任何有关的 Command 或 Connection 对象 (不要忘了对记录集或 连接 调用  Close()   在将它们设置为  = Nothing  之前 )这将缩短数据库必须为您调整资源的时间跨度 并将数据库连接尽可能快地释放给连接池    技巧 进程外的执行将牺牲可靠性 ASP 和 MTS/+ 都有允许您以可靠性换取性能的配置选项 当建立和部署应用程序时 应当理解这种交换   ASP 选项    ASP 应用程序可以配置为以三种方式之一运行 在 IIS 中引入了术语 隔离级 来描述这些选项 三个隔离级值分别是低 中和高   低级隔离 该隔离级在所有版本的 IIS 中受到支持 并且是最快的 它在主 IIS 进程 Inetinfo exe 中执行 ASP 如果 ASP 应用程序崩溃 则 IIS 也将崩溃 (要在 IIS 下重新启动 IIS Web 站点管理员需要使用工具 如 InetMon 来监视站点 如果服务器失败 将运行批处理文件来重新启动服务器 而 IIS 则引入了可靠的重新启动 它将自动重新启动失败的服务器 )  中级隔离 IIS 引入了这个新隔离级 它称为进程外的 这是因为 ASP 运行在 IIS 进程之外 在中级隔离中 所有被配置按 中级 运行的 ASP 应用程序 将共享单个进程空间 这将减少在一个服务器上运行多个进程外的 ASP 应用程序所需的进程数 中级是 IIS 中默认的隔离级 高级隔离 在 IIS 和 IIS 中受到支持 高级隔离也是进程外的 如果 ASP 崩溃 则 Web 服务器并不崩溃 ASP 应用程序将在下一个 ASP 请求时自动重新启动 使用高级隔离 每个被配置为按高级运行的 ASP 应用程序 将在其自己的进程空间中运行 这样可以保护 ASP 应用程序彼此不受干扰 它的缺点是它需要为每个 ASP 应用程序建立独立的进程 当需要在一个服务器上主持十多个应用程序时 会增加很多开销 那么 哪个选项是最好的呢?在 IIS 中 运行进程外的应用程序会极大地影响性能 在 IIS 中 做了许多工作 使得进程外运行 ASP 应用程序对性能产生的影响降到了最低 实际上 在大多数测试中 在 IIS 中的 ASP 进程外应用程序 要比 IIS 中的进程内应用程序运行得更快 无论如何 进程内(低隔离级)在两种平台上仍然产生了最好的性能 但是 如果您的命中率相对较低或最大吞吐量较低 选择低隔离级不会有太大的好处 所以 除非您需要每个 Web 服务器每秒处理数百或数千个页面 否则没有必要选择低隔离级 同样 应当测试多种配置并判断哪种情形最适合您   注意 当您进程外运行 ASP 应用程序(中级或高级隔离)时 则在 NT 上它们将运行在 MTS 中 而在 Windows 上它们将运行在 + 中 即 在 NT 上它们运行在 Mtx exe 中 而在 Windows 上它们运行在 DllHost exe 中 在 任务管理器 中 您可以看见这些正在运行的进程 还可以看见 IIS 如何为进程外的 ASP 应用程序配置 MTS 程序包或 + 应用程序    选项     组件也有三个配置选项 虽然与 ASP 选项不完全相似 组件可以被 不配置 配置为 库应用程序 或配置为 服务器应用程序 不配置 是指不向 + 注册组件 组件将运行在调用者的进程空间 就是说 它们是 进程中 的 库应用程序 也是进程中的 但受惠于 + 的服务 包括安全性 事务和环境支持 服务器应用程序 被配置为在其自己的进程空间中运行   您可能看到 不配置的组件比库应用程序优点稍微多些 您还可能看到 库应用程序 比 服务器应用程序 有很大的性能优点 这是因为 库应用程序 与 ASP 运行在同一个进程中 而 服务器应用程序 则运行在自己的进程中 内部进程调用的开销要比进程内调用的开销大得多 而且 当在进程之间传递数据(如记录集)时 必须在两个进程之间复制所有的数据   缺点!当使用 服务器应用程序 时 如果要在 ASP 和 之间传递对象 请确保对象实现 按值汇集 即 MBV 实现 MBV 的对象将其自身从一个进程复制到另一个进程 这比另一种方式好 在另一种方式中 对象留在创建它的进程中 而其他进程则重复调用创建使用该对象的进程 被断开连接的 ADO 记录集将是按值汇集的 已连接的记录集则不是 Scripting Dictionary 并不实现 MBV 不会在进程之间传递 最后 要另外告诉 VB 程序员的是 MBV 不是通过传递参数  ByVal  获得的 MBV 是由原始组件创作者实现的   怎么办?    如果您想要以性能与可靠性的合理交换来完成您的配置 我们的推荐如下   在 IIS 上 使用 ASP 的低隔离级别 并使用 MTS 服务器包 在 IIS 上 使用 ASP 的中隔离级别 并使用 + 库应用程序 这些是很一般的准则 通常让公司以中或高隔离级别运行 ASP 而单一目的的 Web 服务器可运行于低隔离级别 请权衡折中并自行决定满足需求的配置    技巧 显式使用选项 在 asp 文件中显式使用  选项 Explicit   置于 asp 文件开头的这一指令 强制开发人员声明所有要使用的变量 许多开发人员认为这有助于调试应用程序 因为它避免了错误键入变量名称而不经意地新建变量(例如 MyXLMString= 而非  MyXMLString=)     也许更重要的是 声明的变量比未声明的变量快 实际上 脚本运行时 在每次使用未声明变量时按照名称引用 而声明的变量 在编译或运行时分配了序号 这样 声明的变量按照该序号引用 由于  选项 Explicit  强制变量声明 因此保证声明了所有变量而实现快速访问    技巧 在子例程和函数中使用局部变量 局部变量是在子例程和函数中声明的变量 在子例程和函数中 局部变量访问要快于全局变量访问 使用局部变量还可以使代码更加清晰 因此尽可能使用局部变量    技巧 将常用数据复制到脚本变量 在 ASP 中访问 时 应该将常用的对象数据复制到脚本变量中 这将削减 方法的调用 方法的调用与访问脚本变量相比 要相对昂贵些 在访问 Collection 和 Dictionary 对象时 这一技术也可以削减了昂贵的查找   通常 如果打算多次访问对象数据 请将数据放入脚本变量 该优化的主要目标是 Request 变量(Form 和 QueryString 变量) 例如 您的站点可能传递一个名为 UserID 的 QueryString 假定该 UserID 变量要在特定页中引用 次 请不要调用  Request( UserID )   次 而在 ASP 页的开头将 UserID 赋予某个变量 然后就在页中使用该变量 这将节省 次 方法调用   在实际中 访问 属性或方法暗藏着繁复的过程和大量的开销 下面是一个示例 它只是些相当普通的代码(从语法上讲)   Foo bar blah baz = Foo bar blah qaz( )  If Foo bar blah zaq = Foo bar blah abc Then   在运行这段代码时 将发生下列事件   变量  Foo  被解析为全局变量 变量  bar  被解析为  Foo 的成员 这将产生 方法调用 变量  blah  被解析为  Foo bar  的成员 这也将产生 方法调用 变量  qaz  被解析为  foo bar blah  的成员 是的 这也将产生 方法调用 调用  Foo bar blah quaz( )   又一次产生 方法调用 理解这幅图了吗?  执行步骤 到 将再次解析  baz   系统不知道调用  qaz  是否更改对象模型 因此步骤 到 必须再次执行解析  baz   将  baz  解析为  Foo bar blah  的成员 进行属性置入 再次执行步骤 到 并解析  zaq   再次执行步骤 到 并解析  abc   正如所见 这是非常可怕的低效率(而且非常慢) 用 VBScript 编写该代码实现的快速方法为   Set myobj = Foo bar blah 对 blah 做一次解析  Myobj baz = myobj qaz( )  If Myobj zaq = Myobj abc Then   如果您使用的是 VBScript 或更高版本 则可用  With  语句来写这段代码   With Foo bar blah   baz = qaz( )  If zaq = abc Then  End With    请注意该技巧对 VB 编程同样有效    技巧 避免重新定义数组 尽量避免  Redim  数组 从关心性能的角度来说 如果计算机受物理内存的限制 最好一开始将数组的维数设置为最差方案 而不要将维数设置为最佳方案 再根据需要重新定义维数 这并不意味着明知道不需要那么多而就是应该分配太多的内存   下面代码展示了您没有必要地使用了  Dim  和  Redim  来解决   <%  Dim MyArray()  Redim MyArray( )  MyArray( ) = hello MyArray( ) = good bye lishixinzhi/Article/program/net/201311/12917

Data Transfer Object(数据传输对象)

您正在设计一个分布式应用程序,为了满足单个客户端请求,您发现自己对一个远程接口发出了多个调用,而这些调用所增加的响应时间超出了可接受的程度。

问题

如何保留过程调用接口的简单语义,而不受远程通信固有的滞后时间问题的影响?

影响因素

在与远程对象通信时,请考虑下列需要权衡的因素:

远程调用(那些必须跨越网络的调用)速度缓慢。虽然许多远程调用框架可以隐藏进行远程调用的复杂性,但是它们不能消除发生通信所需的步骤。例如,必须先找到远程对象位置,而且建立与远程计算机的连接,然后才能将数据串行化为字节流,然后可能进行加密,最后才能将其传输到远程计算机。

在考虑网络性能时,必须同时考虑滞后时间和吞吐量。简单地说,"滞后时间"描述了数据的首字节到达目的地之前所经过的时间。"吞吐量"描述了在某个时间段(例如 1 秒)内通过网络发送的数据字节数。在基于 IP 路由的现代网络(例如 Internet)中,滞后时间可以是比吞吐量更大的因素。这意味着,传输 10 字节数据所用的时间可能几乎等于传输 1,000 字节数据所用的时间。在使用无连接协议(如 HTTP)时,此效果尤其明显。通常,网络速度越快可以使吞吐量得以增加,但是,要减少滞后时间则会更加困难。

在设计对象接口时,好的做法是将大量信息隐藏在对象内,并提供一组细粒度方法来访问和 *** 作该信息。"细粒度"意味着每个方法都应该负责单个的、相当小的和基本的功能单位。此方法简化了编程,并提供了对对象内部的更佳抽象,从而增加了重用的可能性。必须根据以下事实对此进行平衡取舍:使用较细粒度的方法意味着需要调用更多的方法才能执行高级别的任务。通常,在同一进程内调用方法时,这些额外函数调用的开销是可接受的;但是,在跨进程和网络边界调用这些方法时,开销可能变得难以接受。

避免远程调用中固有的滞后时间问题的最佳方法是进行更少的调用,并让每个调用传递更多的数据。做到这一点的一种方法是,使用长参数列表来声明远程方法。这样,客户端就可以在单个调用中将更多的信息传递给远程组件。但是,这样做会使针对此接口的编程容易出错,因为程序很可能仅按调用语句中的位置来调用外部方法的参数。例如,如果远程方法接受 10 个字符串参数,则开发人员很容易按错误顺序传递参数。编译器将无法检测到这样的错误。

长参数列表无助于从远程调用向客户端返回更多的信息,因为大多数的编程语言将方法调用的返回类型限制为单个参数。而巧合的是,在传输大多数数据时通常需要返回较多信息。例如,许多用户接口传输少量的信息,却希望返回大量结果数据。

解决方案

创建一个数据传输对象 (DTO),用该对象包含远程调用所需要的所有数据。修改远程方法签名,以便将 DTO 作为单个参数接受,并将单个 DTO 参数返回给客户端。在调用方应用程序收到 DTO 并将其作为本地对象存储之后,应用程序可以分别对 DTO 发出一系列单独的过程调用,而不会引发远程调用开销。Martin Fowler 在 Patterns of Enterprise Application Architecture [Fowler03] 中对此模式进行了说明。

下图显示客户端应用程序如何进行一系列远程调用以检索客户名称的各个元素。

图 1:没有 DTO 的远程调用

DTO 允许远程对象在单个远程调用中将整个客户名称返回给客户端。在此示例中,这样做将使调用次数从 4 次减为 1 次。客户端进行单个调用,然后在本地与 DTO 交互,而不用进行多次远程调用(见图 2)。

图 2:通过使用 DTO 减少调用次数

DTO 是一组需要跨进程或网络边界传输的聚合数据的简单容器。它不应该包含业务逻辑,并将其行为限制为诸如内部一致性检查和基本验证之类的活动。注意,不要因实现这些方法而导致 DTO 依赖于任何新类。

在设计数据传输对象时,您有两种主要选择:使用一般集合;或使用显式的 getter 和 setter 方法创建自定义对象。

一般集合的优点是,只需要一个类,就可以在整个应用程序中满足任何数据传输目的。此外,集合类(例如,简单数组或散列图)内置于几乎所有语言库中,因此您根本不必编写新类的代码。对 DTO 使用集合对象的主要缺点是,客户端必须按位置序号(在简单数组的情况下)或元素名称(在键控集合的情况下)访问集合内的字段。此外,集合存储的是同一类型(通常是最一般的 Object 类型)的项目,这可以导致在编译时无法检测到的微妙但致命的编码错误。

如果为每个 DTO 创建自定义类,则可以提供与任何其他对象完全一样的、客户端应用程序可访问的强类型对象,这样的对象可以提供编译时检查,并支持代码编辑器功能(如 Microsoft® IntelliSense® 技术)。主要缺点是,如果应用程序发出许多远程调用,则您最终可能必须编写大量类的代码。

许多方法试图将这两种方法的优点结合在一起。第一种方法是代码生成技术,该技术可以生成脱离现有元数据(如可扩展标记语言 (XML) 架构)的自定义 DTO 类的源代码。第二种方法是提供更强大的集合,尽管它是一般的集合,但它将关系和数据类型信息与原始数据存储在一起。Microsoft ADONET DataSet 支持这两种方法(请参阅在 NET 中使用 DataSet 实现 Data Transfer Object)。

有了 DTO 类以后,需要用数据填充它。大多数情况下,DTO 内的数据来自多个域对象。因为 DTO 没有行为,因此它不能从域对象提取数据。这是对的,因为如果让 DTO 不知道域对象,您就可以在不同的上下文中重用 DTO。同样,您不希望域对象知道 DTO,因为这可能意味着更改 DTO 将要求更改域逻辑中的代码,这将导致大量维护任务。

最佳的解决方案是使用 Assembler 模式 [Fowler03],该模式可以用业务对象创建 DTO 或者相反。Assembler 是 Mapper 模式的专门实例,在 Patterns of Enterprise Application Architecture [Fowler03] 中也提到过它。

图 3:使用 Assembler 将数据加载到 DTO 中

Assembler 的关键特征是 DTO 和域对象不相互依赖。这就消除了这两种对象的相互影响。不利方面是 Assembler 同时依赖于 DTO 和域对象。对这些类的任何更改都可能导致必须更改 Assembler 类。

示例

请参阅在 NET 中使用 DataSet 实现 Data Transfer Object。

测试考虑事项

DTO 是简单对象,它不应该包含需要测试的任何业务逻辑。但是,您确实需要测试每个 DTO 的数据聚合。每个 DTO 可能需要测试,也可能不需要,这取决于您的序列化机制。如果序列化是框架的一部分,则只需要测试一个 DTO。如果不是这样,请使用一般的反射机制,这样就不需要测试每个 DTO 的序列化。

DTO 还对远程函数的可测试性有好处。通过使远程方法的结果能够在对象实例中使用,可以轻松地将此数据传递到测试模块,或将其与所需结果进行比较。

安全考虑事项

理想情况下,应该先筛选和验证从不可靠的来源获得的数据(如来自 Web 页的用户输入),然后将其置于 DTO 中。通过这样做,就可以认为 DTO 中的数据是相对安全的,从而简化了将来与 DTO 的交互。

接收 DTO 的进程和关联用户的安全凭据也是值得注意的。DTO 通常包含从许多不同来源聚集在一起的大量信息。您是否已授权 DTO 的所有用户访问 DTO 所包含的所有信息?确保用户已得到授权的最佳方法是仅使用用户安全凭据所允许的特定数据填充 DTO。努力避免让 DTO 负责自己的安全性。这将增加 DTO 对其他类的依赖数,这意味着必须将这些类部署到使用 DTO 的所有节点。这还会将安全性功能分散到更多类中,从而增大了安全风险,并对灵活性和可维护性产生负面影响。

结果上下文

Data Transfer Object 具有下列优缺点:

优点

减少了远程调用次数。通过在单个远程调用中传输更多的数据,应用程序可以减少远程调用次数。

提高了性能。远程调用可以使应用程序的运行速度大大降低。减少调用次数是提高性能的最佳方法之一。在大多数方案中,传输大量数据的远程调用所用的时间与仅传输少量数据的调用所用的时间几乎相等。

隐藏内部情况。在单个调用中来回传递更多的数据,还可以更有效地将远程应用程序的内部情况隐藏在粗粒度接口的背后。这就是使用 Remote Facade 模式 [Fowler03] 的主要原因。

发现业务对象。在一些情况下,定义 DTO 有助于发现有意义的业务对象。在创建用作 DTO 的自定义类时,您通常会注意到作为一组凝聚性信息而显示给用户或另一个系统的元素分组。通常,这些分组用作描述应用程序所处理的业务域的对象的有用原型。

可测试性。将所有参数封装到可序列化对象中可以提高可测试性。例如,可以从 XML 文件中读取 DTO,并调用远程函数以测试它们。同样,可以轻松地将结果再序列化为 XML 格式,并将 XML 文档与所需结果进行比较,而不必创建冗长的比较脚本。

缺点

可能需要太多的类。如果选择了使用强类型的 DTO,则可能必须为每个远程方法创建一个(如果考虑返回值,则为两个)DTO。即使在粗粒度接口中,这也可能导致大量的类。编写如此数量的类的代码并管理这些类会是很困难的。使用自动代码生成可以在一定程度上缓解此问题。

增加计算量。如果将服务器上的一种数据格式转换为可以跨网络传输的字节流,并在客户端应用程序内转换回对象格式,可以带来相当大的开销。通常,需要将来自多个源的数据聚合到服务器上的单个 DTO 中。要提高通过网络进行远程调用的效率,必须在任一端执行其他计算,才能聚合和串行化信息。

增加编码工作量。可以用一行代码完成将参数传递到方法的 *** 作。使用 DTO 要求实例化新对象,并为每个参数调用 setters 和 getters。编写此代码可能是很乏味的。

这个主要和代码底层原理有关,函数调用被编译器编译后,会比代码展开(内联或者宏调用)多个跳转指令,也就是代码执行要从一个内存位置跳转到另一个位置去执行;函数调用还有另外的性能开销,那就是函数栈,函数调用需要开辟出栈空间,还要对函数参数进行压栈、出栈 *** 作。关键的问题是,如果只是进行一次函数调用,所有的这些性能开销加起来,和代码展开相比其实也没有什么差别(性能测试除外),但是需要代码展开的都是要反复、多次调用执行的小代码段,多次的执行相比起来函数调用和代码展开的性能差别就很大了。

1记住阿姆达尔定律:funccost是函数func运行时间百分比,funcspeedup是你优化函数的运行的系数。所以,如果你优化了函数TriangleIntersect执行40%的运行时间,使它运行快了近两倍,而你的程序会运行快25%。这意味着不经常使用的代码不需要做较多优化考虑(或者完全不优化)。这里有句俗语:让经常执行的路径运行更加高效,而运行稀少的路径正确运行。2代码先保证正确,然后再考虑优化这并不意味着用8周时间写一个全功能的射线追踪算法,然后用8周时间去优化它。分多步来做性能优化。先写正确的代码,当你意识到这个函数可能会被经常调用,进行明显的优化。然后再寻找算法的瓶颈,并解决(通过优化或者改进算法)。通常,改进算法能显著地改进瓶颈——也许是采用一个你还没有预想到的方法。所有频繁调用的函数,都需要优化。3我所了解的那些写出非常高效代码的人说,他们优化代码的时间,是写代码时间的两倍。4跳转和分支执行代价高,如果可能,尽量少用。函数调用需要两次跳转,外加栈内存 *** 作。优先使用迭代而不是递归。使用内联函数处理短小的函数来消除函数调用开销。将循环内的函数调用移动到循环外(例如,将for(i=0;i<100;i++)DoSomething();改为DoSomething(){for(i=0;i<100;i++){…}})。if…elseif…elseif…elseif…很长的分支链执行到最后的分支需要很多的跳转。如果可能,将其转换为一个switch声明语句,编译器有时候会将其转换为一个表查询单次跳转。如果switch声明不可行,将最常见的场景放在if分支链的最前面。5仔细思考函数下标的顺序。两阶或更高阶的数组在内存中还是以一维的方式在存储在内存中,这意味着(对于C/C++数组)array[i][j]和array[i][j+1]是相邻的,但是array[i][j]和array[i+1][j]可能相距很远。以适当的方式访问存储实际内存中的数据,可以显著地提升你代码的执行效率(有时候可以提升一个数量级甚至)。现代处理器从主内存中加载数据到处理器cache,会加载比单个值的数据。该 *** 作会获取请求数据和相邻数据(一个cache行大小)的整块数据。这意味着,一旦array[i][j]已经在处理器cache中,array[i][j+1]很大可能也已经在cache中了,而array[i+1][j]可能还在内存中。

非内联函数调用的过程:

调用函数实际上将程序执行顺序转移到函数(转移到存放该函数的内存中某个地址),将函数的程序内容执行完后,再返回到转去执行该函数前的地方。

引入内联函数的目的是为了

解决程序中函数调用的效率问题。

函数是一种更高级的抽象。它的引入使得编程者只关心函数的功能和使用方法,而不必关心函数功能的具体实现;

函数的引入可以减少程序的目标代码,提高程序的模块化,实现程序代码和数据的共享。但是,函数调用也会带来降低效率的问题,因为调用函数实际上将程序执行顺序转移到函数(转移到存放该函数的内存中某个地址),将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移 *** 作要求在转去前要

保护现场并记忆执行的地址

,转回后先要

恢复现场,并按原来保存地址继续执行

。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。特别是对于一些函数体代码不是很大,但又频繁地被调用的函数来讲,解决其效率问题更为重要。引入内联函数实际上就是为了解决这一问题。

在程序编译时,

编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替换

。显然,这种做法不会产生转去转回的问题,但是由于在编译时将函数中的代码替代到程序中,因此会增加目标程序代码量,进而增加空间开销,而在时间代销上不象函数调用时那么大,可见它是以目标代码的增加为代价来换取时间的节省。

理解上,可以类比C中的宏定义,或者typedef的用法来理解,实质上,就是起了个别名

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/12181530.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-21
下一篇 2023-05-21

发表评论

登录后才能评论

评论列表(0条)

保存