网络算法实现原则.docx
《网络算法实现原则.docx》由会员分享,可在线阅读,更多相关《网络算法实现原则.docx(15页珍藏版)》请在冰豆网上搜索。
网络算法实现原则
网络算法实现原则
标题页
本章介绍作者归纳的15条实现原则。
这些实现原则是从比较成功的协议实现中归纳出来的。
其实许多实现者已经有意无意地使用了这些原则,本章只是更清楚地将它们表达出来,以使实现者可以更加主动地去运用它们。
首先用两个例子说明为什么要使用原则。
3.1运用实现原则的例子:
更新TCAM
第一个例子是关于用TCAM做IP地址查找。
第一节课我们介绍了基于多分支trie的IP地址查找算法DIR24-8及其硬件实现。
实现IP地址查找还有一种非算法的方法,就是使用一种特殊的存储器,叫做内容可寻址存储器。
常规的存储器:
给出地址,得到存储于该地址的内容。
内容可寻址存储器:
给出查找关键字,得到匹配该关键字的条目序号。
使用TCAM进行IP地址查找
图3.4是TCAM用于IP地址查找的例子。
这里我们使用简化表示,每个地址前缀用长度为32比特的三态字符串表示,通配符都在字符串的尾部。
按照TCAM的工作原理以及最长前缀匹配的要求,TCAM中的地址前缀按照前缀长度从大到小的顺序排列。
路由表是动态变化的,经常会需要在TCAM中添加或删除一条地址前缀,与此同时仍然保持地址前缀按照前缀长度从大到小的顺序排列。
假定TCAM是向上扩展空间的,最朴素的方法是在长度为2的前缀表项中插入前缀11*,比如插在前缀0*的前面,为此,需将前缀10*至前缀010001*整体向上移动一个位置。
对于包含大量路由表项的路由器来说,这种更新的速度太慢了。
有没有快一点的方法呢?
我们再来看一下图中的前缀排列方法:
……。
如果地址前缀必须要这么排列,那我们没有什么好办法,只好用朴素的方法来解决。
但是我们不甘心,那么就要仔细想想这么排列是否有必要呢?
首先,我们考虑相同长度的前缀按照大小排列,对于TCAM执行最长前缀匹配是不是必须的?
一个IP地址有没有可能同时匹配两个相同长度的前缀?
这是不会的,因此相同长度的前缀之间不需要有序,这是一个可以利用的自由度。
所谓自由度就是允许我们改变的量。
理解并利用自由度
(1)
利用该自由度,我们可将11*插入到00*和111*之间。
当然,如果为此将111*及以上前缀向上移动一个位置,就没有什么意义了。
我们的想法是将111*移出,将11*插入111*的位置,然后再为111*寻找一个插入位置。
尽管这个问题和原来的差不多,仍然要向TCAM中插入一条前缀,但是问题的规模缩小了一点。
我们可以采用类似的方法插入111*。
显然我们可以采用递归的思想来设计一个算法。
使用算法技术—采用递归
实现的时候展开递归:
……。
如果每一种长度的前缀都有(即最坏情况),需要(32-i)次访存。
对于较小的i,访存次数接近32。
这个算法已经比朴素的算法好了很多,还能再改进吗?
还能减小最坏情况下的访存次数吗?
(想一想)
我们再来看这张图,这张图假设空闲空间在TCAM的顶部,这是不是必须的?
实际上空闲空间可以放在TCAM的任何地方,因此空闲空间的位置也是一个设计自由度。
进一步利用自由度
利用这个自由度,可以将空闲空间放在中间,比如放在长度为16的前缀项后面,这时最坏情况下的访存次数可以减少一半。
当然,空闲空间的数量也可以是一个自由度,可以进一步减少最坏情况下的访存次数。
除了空闲空间的位置及数量之外,还有没有可以利用的自由度了呢?
提示:
长的前缀是否一定要出现在短的前缀之前?
比如,010*能否放在111001*之前?
完全可以,因为不可能有一个地址会同时匹配这两项。
一个更复杂的自由度是,“如果i>j,那么长度为i的前缀必须出现在长度为j的前缀之前”,这是一个充分条件,但不是必要的。
一个不那么严格的要求是,“如果两个前缀P和Q可能匹配同一个地址,且P比Q长,那么P必须出现在Q之前”。
这样修改规范后,可以进一步减少最坏情况下的访存次数。
缺点是提高了复杂度。
以这么复杂的方法来减少访存次数是否划算值得商榷,但它指出了一个重要的原则,即放宽要求。
我们经常将一个大的问题划分为较小的子问题,然后将子问题及相关的规范交由相关人员去解决。
比如,TCAM的硬件设计人员可能将更新问题交给写微代码的人去完成,要求将长的前缀放在短前缀的前面。
但是这个规范可能不是解决原始问题的唯一方法,改变规范(P3放宽要求)可能产生更有效的解决方案。
当然,这需要有具有好奇心和自信的设计人员,他理解整个大问题,并且足够勇敢来提出危险的问题。
这个例子是想告诉我们,要去寻找和利用自由度来优化实现。
3.2算法vs算法学
下面这个例子用来说明算法与算法学的差异。
举例:
安全物证问题
假设一个入侵检测系统通过统计流量来检测异常节点,比如在一个测量周期内发现一个节点向网络中的不同机器发送了10万个包,就确定这个节点在进行端口扫描攻击。
当判定某个节点为攻击源时,要将该节点在测量周期内发送的包写入安全物证日志,供管理员进一步分析。
如何检测攻击不是我们要讨论的,我们关心的是当判定某个节点为攻击源时,如何得到它已经发送过的那些包。
解决方案
为实现此目的,我们维护一个包队列,当转发一个数据包时,路由器同时将该包放入队列中。
为限制队列的长度(路由器的内存是有限的),当队列满时,删除队尾的包。
该方案的主要困难是,当检测到一个可疑流时,该流已经有大量的数据包在队列中了,并且和大量其它的包混在一起。
如何高效地从队列中找到属于可疑流的所有包?
最相素的方法是顺序搜索队列,这显然非常低效。
我们一般会采用什么方法,快速找到流F的数据包呢?
教科书上的算法
教科书上的算法会构造某个索引结构来快速搜索流ID。
比如,我们可以维护一个流ID的哈希表。
……
但是该算法仍有问题。
它需要额外的空间来维护哈希表和指针列表,而对于高速实现来说存储空间是非常宝贵的。
它还增加了包处理的复杂度,需要维护哈希表。
这个算法的问题在哪里?
就是我们潜意识里假设每一个流都有可能是可疑流,因此,每一个包到来时都将其分到相应的流中。
这样,一旦某个流被认定是可疑流,立即就可以得到属于该流的所有数据包。
但事实上,可疑流只是少数,这里面我们做了许多无用的处理,将属于正常流的包也都组织在哈希表中了。
比如,10万个流中只有一个流是异常的,但我们将10万个流的包都做了归类,这是显而易见的浪费。
那么我们怎么可以避免做无用功呢?
联想到我们前面举过的例子(检测异常URL),我们可以采用推迟计算来解决这个问题。
系统的解决方案
基本思想:
将判断一个包是否属于可疑流这件事情,拖到不得不做时再去做。
什么时候不得不做?
(1)发现了可疑流;
(2)数据包将移出队列
令路由器转发包时统计每个流(节点)发送的包数。
当流F发送的包数超过一个阈值(如10万)时,将流F添加到可疑节点列表中。
当可疑节点列表非空且队列满时,每当往队列中添加一个包,就从队尾中移出一个包,判断该包是否属于F。
若属于F,将该包(或指针)拷贝到物证日志中;若否,将该包丢弃。
该方案不需要维护哈希表和指针列表,节省了大量的存储空间和计算开销。
3.3十五条设计原则
第一章的例子(设计一个检测异常URL的芯片)和前面这两个例子给了我们有关网络算法学的一些初步体验,下面介绍作者归纳的15条原则,这15条原则我们会不断地使用。
这15条原则可以分为三类:
1)系统原则:
第1-5条原则利用了系统思想,它将系统看成是由子系统构成,而不是一个黑盒子。
2)兼顾模块化和效率:
第6-10条原则允许保留复杂系统的模块化,与此同时给出提高性能的方法。
3)加速:
第11-15条原则提出了加速某个关键功能的技术(仅考虑该功能本身)。
每条原则会用一个例子来说明,具体细节将在后续章节中讨论。
P1:
避免常见情形中的明显浪费
在一个系统中,在一些特殊的操作序列中可能存在资源浪费。
如果这些模式经常出现,就有必要去除这个浪费,这样可以从整体上降低系统的开销。
编译器的例子:
消除重复的子表达式。
经典的网络例子:
操作系统和用户空间之间多次数据包拷贝(第五章介绍)。
这条原则看起来容易做起来难,最难的是如何发现明显的浪费。
这里,每一步操作孤立地来看都没有问题,而是由于特殊的操作序列导致了浪费。
显然,暴露的上下文越大,越可能发现产生浪费的操作序列(比如数据包拷贝),因而这里需要系统思维。
P3:
放宽系统要求
系统通常自顶向下设计,先将功能在子系统间划分,然后确定对子系统的需求和接口,最后设计和实现每一个子系统。
当遇到实现困难时,有些方面的要求可以改变。
在图3.8中,一个系统由两个子系统组成,子系统2对子系统1的实现要求称为规范S。
如果子系统1实现困难,有时可以降低其实现要求,即要求子系统1遵循较为宽松的规范W。
需要注意的是,放宽对一个子系统的实现规范,必定会对另一个子系统有更高的要求,即子系统2必须遵循更强的特性(P->Q)。
在第1章的例子中,由于设计除法电路(subsystem1)比较复杂,改用移位代替除法,也就是放宽了子系统1的实现规范(S->W):
子系统1只需实现移位操作而不是任意的除法操作。
但是其上层子系统必须用2的幂次表示门限,而不是一个任意的浮点数。
P3a:
牺牲确定性换时间
超级节点的检测:
确定性的方法是统计以每一个节点作为源(或目的)的数据包数量,这需要检查每个包的源地址(或目的地址)进行统计,这在高速网络中是做不到的。
随机化方法是每隔一定数量的包统计一次,虽然不能保证百分之百正确,但在很大概率上是正确的。
这里的难点是根据可接受的虚警率和漏检率来估计判定门限,要考虑到数据包流的到达时间分布等,大量的研究工作集中在这个方面。
P3b:
牺牲精度换时间
图像压缩是一种很耗时的操作,实时视频要求压缩速度很快。
离散余弦变换可将信号能量集中在少数几个变换系数上,然后只需对主要的变换系数进行量化编码,达到快速压缩的目的。
但是图像质量(精度)会受到一些影响。
P3c:
在空间中移动计算
在空间中移动计算是指将计算从一个子系统移动到另一个子系统(注意和前两条原则的不同)。
比如,数据包分片会影响路由器的吞吐量,IPv6将分片的功能从路由器移到了源节点。
源节点主动探测端到端路径上的MTU,确定合适的分组大小,路由器不再提供分片的功能。
这样就简化了路由器的实现。
当然,这里的移动已经超出了一个网络设备的限制,是在整个网络中的移动(如果将网络看成一个系统,则路由器、主机就是子系统)。
P4:
利用系统组件
系统设计采用黑盒视图,就是将系统分解为若干子系统,然后独立地设计每一个子系统,而不去关心其它子系统的内部细节。
尽管这种自顶向下的方法具有很好的模块化,但实际上,性能关键的组件在构建时通常部分地采用“自底向上”的方法构建。
比如,要在一个硬件上设计算法,通常要让算法适应硬件的特性,而不是让硬件来适应算法。
P4b:
用空间换速度
一个显而易见的技术是用较大的空间来减少计算时间,比如将函数计算变为查表。
一个不那么显而易见的技术是用较小的空间来提高速度。
比如,把一个很大的数据结构压缩到可以放入cache,或者大部分可以放入cache,可以极大地提升系统速度。
当一个表的规模很大时,该技术特别有效。
有人可能会问,空间和时间都节省了,怎么会有这样的好事?
事实上,计算复杂度是增加的。
比如,访问压缩的数据结构,其处理复杂度是提高的。
但由于现代处理器的计算速度比访存速度快得多得多,在多核处理器上这个差距更大。
在这种情况下,适当增加计算复杂度并不会使处理器成为瓶颈,却因为提升了访存速度使得系统整体性能得到提升。
这是实践中用得很多的一种优化技术。
这也启示我们,在优化系统时一定要搞清楚瓶颈在什么地方,如果计算不是瓶颈,那么减少计算并不能提高系统速度。
如果P4原则使用过度,系统的模块化将受到损害。
为此,需要注意两个问题:
(1)如果我们利用其它系统特性只是为了提高性能,那么对那些系统特性的改变应当只影响性能,不影响正确性。
(2)我们仅对确认为是系统瓶颈的组件运用该技术。
以上表明,对系统的优化要有一定的度,不是越多越好。
对系统修改越多,可能产生的不可预知的相互作用就会增多,可能会对系统的正确性产生影响,所以应当只对系统做最小的修改(不提倡大刀阔斧的改)。
P5:
增加硬件提高性能
当所有的方法都不奏效时,增加硬件(如使用更快的处理器、存储器、总线、链路等)可能是更简单和有效的方法。
然而,硬件毕竟成本比较高,即使需要增加硬件,我们也希望只增加最少的硬件。
这就要求充分挖掘软件的潜力,把软件的性能做得极至。
随着处理器的性能越来越强大,存储容量越来越大,我们发现软件算法设计得好的话,也是可以运行得很快的。
因此,一种比较理想的情况是,用软件实现的关键算法不需修改,就可以随着处理器的升级自然获得速度提升。
当然这不是一件轻而易举的事,因为针对不同的硬件,算法通常需要一些调整。
哪些功能在硬件上实现,哪些功能在软件上实现,是有一些讲究的。
一般认为,硬件(不包括处理器)缺乏灵活性(不容易增加新的功能),且设计成本高、周期长,适合完成较简单的、较固定的功能。
软件灵活性好,可以很容易地移植到新的、更快的处理器上,获得性能提升。
但是动态可重构技术的出现使得这种界线在逐渐模糊,利用动态可重构FPGA实现的系统,既有硬件的执行速度,又有软件的可编程性,可以执行复杂的功能(可重构处理器),设计工具的出现也使得硬件设计周期大大缩短了。
当然现在用FPGA构造系统还比较贵,成本可能会是一个考虑的因素。
总之,在是否增加硬件、增加什么硬件方面,需要有通盘的考虑。
以下特殊的硬件技术通常用于网络ASIC芯片中。
P6:
用高效的定制例程替换低效的通用例程
通用性和高性能通常是一对矛盾。
通用性要求兼顾大多数情形,一般来说就不会针对某种情形进行特殊设计。
所以,一个“放之四海皆宜”的东西一定不是最高效的。
当有些情形很重要时,有必要针对它设计定制的例程。
比如,操作系统的cache替换策略是LRU,将最长时间未被访问的数据记录替换出去,这么做符合一般的程序运行模式(局部性原理)。
然而,考虑一个查询处理例程,它在处理一个数据库查询请求时,需要依次地处理一系列的数据库记录。
在这种情形下,最近使用过的数据库记录恰恰是最不会被再次访问的,因此应当选择这样的记录替换出去。
正因为如此,许多数据库应用都用定制的缓存例程替换了操作系统的缓存例程。
为避免代码膨胀,最好只对关键的例程进行定制化。
P7:
避免不必要的一般性
适应各种情形的一般性子系统具有很多特性,当它只应用于特定情形时,一些不必要的或很少使用的特性会影响性能。
我们可以通过移除一些不必要的特性来提高性能。
P8:
不要受参考实现的束缚
我们实现一个协议或系统时,都要遵循相应的规范。
规范是解释性的,主要解释概念、功能、流程等。
严格来说,规范应当使用规范语言来书写,它描述要做什么,但不包括怎么做。
但是因为规范语言用得不普遍,很多人不懂,因此现实生活中许多规范会用命令式语言(如C语言)给出,即参考实现。
这些参考实现给出了如何实现功能的代码。
使用参考实现来描述规范有两个副作用,一是描述过于详细(规范应当只说要做什么,不应包括怎么做),二是使用者盲目相信参考实现,可能直接将参考实现的代码拷贝到自己的系统中。
但是,参考实现代码只是为了解释概念,并不关注效率,因此直接使用参考实现会非常低效。
这是实现者很容易犯的错误。
P9和P10:
这里暂不举例(没有找到可以三言两语讲清楚的例子),下一章结合具体的例子再解释。
P11:
优化预期情形
尽管系统可能会呈现出很多种行为,但是大部分情况下,系统的行为是可预期的。
比如,一个设计良好的系统大部分时间工作在无故障的状态;网络中的数据包大部分情况是按序到达的,并且没有出错。
我们有必要去优化这些常见的情形,哪怕使得非预期情形的处理变得低效。
优化常见情形的方法通常称为启发式方法。
启发式方法一般不被理论家待见,他们更倾向于那些能够用平均指标或最坏指标精确量化的机制。
然而,在实际的计算机系统中,启发式方法被大量使用。
(性能才是硬道理)
确定常见情形主要依靠设计人员的直觉,也可以通过测量工具来发现。
通常,每个数据包都要进行的操作可视为常见情形。
P12:
增加或利用状态
如果一个操作的代价很高,可以考虑维护额外的(冗余的)状态来加速该操作。
数据库中的一个典型例子是使用辅助索引。
比如,银行记录可能使用客户的身份证号作为主键进行存储和查找,但是很多时候需要根据客户名字进行查询。
为了加快这种操作,需要利用客户名字建立另外一个索引(如哈希表、B-树)。
增加状态不仅带来空间上的开销,还有时间上的开销,比如需要维护状态。
有时候可以不增加新的状态,而是利用已有的状态,这种技术称为增量计算。
比如,在计算IP头检验时,头部只有几个域有变化,可以进行增量计算(不需要对所有的域计算检查和)。
P13:
优化自由度
自由度是可以由实现者控制的变量,知道哪些变量可控,那么我们的问题就变为:
通过优化这些变量来最大化性能。
运用这条原则的难点是找到自由度。
P15:
利用算法技术构造高效的数据结构
在有些情形中,高效的算法肯定可以极大地提高系统的性能。
但是必须注意,在任何算法问题成为瓶颈前,需要先运用P1~P14原则对系统进行优化。
换句话说,在其它问题没有解决前,算法通常不是瓶颈。
因此我们在科研实践中,通常先考虑数据路径上其它方面的优化。
在所有其它问题都解决后,剩下的才是算法问题。
经常有学生觉得不做算法就不算做研究,做的工作就没有技术含量。
但是系统领域追求的是简洁优雅的解决方案,用最少的修改把问题解决好是最高境界,因为这样的工作离实用最接近。
所以我们在做系统优化时,遵循的一个原则是修改越少越好;在达到相同效果的前提下,方法越简单越好。
这也是操作系统中很少用复杂算法的原因,太复杂的算法会影响系统的稳定性和可靠性。
算法方法包括使用标准的数据结构,以及一般的算法技术,如分治和随机化。
需要注意的是,设计的算法可能随着系统结构和技术的变化而被淘汰。
真正的技术突破来自算法思维的运用,而不仅仅是重用已有的算法。
3.4一些警告
在运用原则前,必须理解重要的性能指标,确定系统瓶颈。
在完成改进后,必须通过实验来确认改进的效果。
下面用两个例子进行说明。
案例一:
减少网页下载时间
在图3.8中,web客户欲从服务器获取内嵌有图像的一个网页。
典型地,客户先发送对网页的GET请求,然后针对内嵌的每个图像分别发送GET请求。
对原则P1(消除明显的浪费)的自然运用是:
为什么服务器不自动将图像和网页一起发送给客户,而要等待客户一个一个来请求呢?
这应当可以减少网页的下载延迟。
为了测试这个猜想,作者修改了服务器软件,令服务器自动发送图像给客户,然后测量性能。
出乎意料的是,网页下载延迟的改进很微小。
使用一个基于tcpdump的网络分析工具,作者发现有两个原因导致这个看似不错的改进方法是个坏主意。
(1)与TCP的相互作用:
web传输建立在TCP之上。
TCP在新建立的连接上使用慢启动发送数据包:
首先发送一个TCP段,在收到确认后发送两个TCP段,在收到ACK后逐步提高发送速度。
因此,即使令服务器主动发送图像,发送端TCP也必须等待ACK来提高发送速度,这和等待客户的图像请求没有多大的区别。
(2)与内容缓存的相互作用:
许多客户端有缓存功能,一些常用的图像会缓存在本地,因此令服务器主动发送已经缓存在本地的图像是对带宽的浪费。
如果让客户发送请求,则客户对于已经缓存在本地的图像不会发送GET请求。
从这个例子我们了解到,如果对系统各个部分之间的相互作用不清楚,那么优化的效果可能达不到我们的预期。
这里最困难的地方就是很多时候我们不能清楚地理解复杂的相互作用,这时候实验验证就很重要了。
案例二:
加速基于特征的入侵检测
许多网站安装有入侵检测系统,目前最流行的入侵检测系统是snort。
Snort根据系统中安装的一组规则来检测攻击。
图示为snort的一条规则,包括如下几个部分:
●动作:
包括pass、alert、log三种。
●协议:
包括TCP、UDP、ICMP三种。
●源网络:
通常是除本地网络之外的所有网络。
●源端口:
本规则为any。
●目的网络:
通常是本地网络
●目的端口:
本规则为80。
●Msg:
如果匹配该条规则,则给出该条提示。
●Flags:
匹配该规则的TCP包,这些标志必须置位。
●Content:
包载荷中包含的特征字符串,可能不止一个。
●Nocase:
表示特征串是大小写无关的
和防火墙的过滤规则只匹配IP地址、端口号、协议、TCP标志位等不同,snort的规则还要匹配数据包载荷中指定的字符串。
经统计,84%的snort规则中包含字符串。
实验表明,字符串匹配是snort中开销最大的操作,占整个执行时间的31%。
Snort匹配规则的方法
作者发现,匹配某个过滤器的规则数可能很多,比如匹配80端口的规则多达310条(大量攻击是利用web进行的),逐条规则地运用BM算法查找字符串,开销很大。
P1原则的简单应用
作者将规则集中所有的字符串抽出来组织到一个自动机中,修改了字符串匹配算法,使得用包内容作为输入,一遍扫描就能找出所有匹配的字符串(给出字符串编号)。
作者将这个新算法应用到snort的整个规则集上,将随机生成的字符串输入到自动机中进行测试,发现性能为snort中字符串搜索算法的50倍。
于是,作者将这个新算法集成到snort中,并使用一个包踪迹文件(用tcpdump从网络中抓真实的包,存在文件中)作为输入进行测试,发现新算法在性能上几乎没有改进!
原因和教训
作者仔细分析后,认为有两个方面的原因可以解释该问题:
(1)实验使用的包踪迹文件包含了很少的web流量,在匹配了过滤器后只有很少的数据包还要匹配多条规则,因此多字符串匹配在该踪迹文件上不是瓶颈。
当作者换用仅包含web流量的包踪迹文件进行测试时,性能提高非常明显。
也就是说,算法性能与流量模式有很大的关系,不是所有时候都有效。
(2)cache的影响:
多字符串查找需要使用一个数据结构(如trie),其大小随字符串数量的增大而增大。
研究发现,当字符串数量超过100条时,数据结构已经大到无法放入cache中,而Snort中包含的字符串数量达到几千条。
也就是说,算法性能还和规则集的大小有关。
如果不考虑问题的规模,仅仅将单字符串匹配换成多字符串匹配,并不一定能提高性能。
这种情况下,不如将9张表中的字符串单独构造自动机,用一批包依次匹配9张表,可能效果更好。
教训:
有时候声称的改进并没有针对真正的瓶颈(如大多数情况是单串匹配),造成优化效果不好;如果与系统的其它部分产生相互作用(如cache),性能优化的效果也可能不好。
所以,首先要确认系统真正的瓶颈是什么,不是瓶颈的部分,实现再低效都不用管它;其次要分析与系统其它部分的相互作用,以免改进带来的好处与产生的坏处相互抵销,甚至更糟。
后者是难点。
八个提醒注意的问题
从以上两个案例,作者提出以下8个需要注意的问题,避免不恰当地运用实现原则。
Q2:
这确实是一个瓶颈吗?
80-20规则表明,80%的性能提升来自于对系统20%的优化,因此只需要找到最关键的瓶颈就行了,其它次要的问题可以不用管。
这可以使用如Oprofile这样的工具来发现。
Q4:
初步分析表明会有重大的改进吗?
在动手进行一个完整的实现前,快速地分析一下可以获得多大的收益,以避免做无用功。
对于终端系统而言,由于访存是最大的瓶颈,因此用访存次数做为粗略的估计是合理的。
Q5:
值得增加定制硬件吗?
定制硬件需要增加成本,在性价比不断提升的通用处理器上利用软件算法获得高性能,无疑是一种非常有吸引力的做法。
通常认为定制硬件设计周期长,开发成本高。
但是随着高效的芯片