acm-header
登录

ACM通信

实践

非统一内存访问概述


蜿蜒的道路

来源:iStockPhoto.com

回到顶部

非均匀内存访问(NUMA)是指处理器地址空间中各个点的内存具有不同的性能特征。在当前的处理器速度下,从处理器到内存的信号路径长度起着重要作用。增加信号路径长度不仅增加了对内存的延迟,而且如果信号路径由多个处理器共享,还会迅速成为吞吐量瓶颈。在数据路径跨越主板或底盘的大型系统上,与内存的性能差异首先是明显的。这些系统需要具有NUMA支持的修改操作系统内核,该内核显式地理解系统内存(例如内存区域所在的机箱)的拓扑属性,以避免过长的信号路径长度。(Altix和UV, SGI的大地址空间系统,就是例子。这些产品必须修改Linux内核以支持NUMA;在这些机器中,多个机箱中的处理器通过名为NUMALINK的专有互连连接起来)。

如今,处理器速度非常快,通常需要将内存直接连接到它们所在的套接字上。从一个套接字到另一个套接字的内存访问对于访问本地内存有额外的延迟开销——它需要首先遍历内存互连。另一方面,与远程内存访问相比,从单个处理器到本地内存的访问不仅具有较低的延迟,而且不会导致互连和远程内存控制器上的争用。最好避免远程内存交流。正确地放置数据将增加总体带宽和内存延迟。

随着通过使内存更接近处理器核心来提高系统性能的趋势继续发展,NUMA将在系统性能中发挥越来越重要的作用。现代处理器有多个内存端口,访问内存的延迟甚至只取决于芯片上相对于控制器的核心位置。未来几代处理器将在性能上有越来越大的差异,因为芯片上更多的核心需要更复杂的缓存。随着这些不同类型内存的访问属性不断分化,操作系统可能需要新的功能来实现良好的性能。

现在的NUMA系统大多在多套接字系统上遇到。如今,典型的高端商务类服务器都带有两个套接字,因此将有两个NUMA节点。内存访问(随机访问)的延迟大约为100ns。对远程节点上的内存访问又增加了50%。

对性能敏感的应用程序可能需要复杂的逻辑来处理具有不同性能特征的内存。如果开发人员出于性能原因需要显式控制内存的位置,一些操作系统为此提供api(例如,Linux、Solaris和Microsoft Windows提供NUMA的系统调用)。但是,已经在管理内存访问的操作系统中开发了各种启发式方法,以允许应用程序透明地利用底层硬件的NUMA特性。

NUMA系统将内存分类为NUMA节点(Solaris称之为地方组织).一个节点中的所有可用内存对于特定处理器具有相同的访问特性。节点与处理器和设备具有亲和性。这些设备可以在NUMA节点上使用性能最好的内存,因为它们是本地连接的。内存叫做节点本地如果它是从NUMA节点分配的,那对处理器来说是最好的。例如,NUMA系统显示在图1有一个节点属于一个套接字,每个套接字有四个核。

从系统中可用的NUMA节点分配内存的过程被调用NUMA位置。由于位置只影响性能而不影响代码的正确性,所以启发式方法可以产生可接受的性能。在非缓存相关NUMA系统的特殊情况下,这可能不是真的,因为写入可能不会以正确的顺序到达内存。然而,在为非缓存一致的NUMA系统编写代码时,它们面临多种挑战。这里我们只讨论常见的缓存相关NUMA系统。

这些讨论的重点将主要集中在Linux上,因为它是一个具有精致NUMA设施的操作系统,目前在性能关键的环境中被广泛使用。作者参与了Linux中NUMA功能的创建,对这些功能非常熟悉。

Solaris也有一些类似的特性,一个但部署的系统数量却少了几个数量级。为其他类unix操作系统添加支持的工作正在进行中,但到目前为止,这种支持主要局限于用于放置内存访问的操作系统调优参数。微软Windows也有一个开发的NUMA子系统,它允许有效地放置内存结构,但该软件主要用于企业应用程序,而不是高性能计算。企业级应用程序对内存访问速度的要求通常比高性能计算更宽松,这意味着与Linux相比,Windows在NUMA内存处理上花费的精力更少。

回到顶部

操作系统如何处理NUMA内存

现代生产操作系统允许对NUMA进行管理的主要类别包括:接受性能不匹配、硬件内存分条、启发式内存放置、静态NUMA配置和应用程序控制的NUMA放置。

忽视的区别.由于NUMA放置是一种最有效的方法,一种选择是简单地忽略可能的性能优势,只对待所有内存,就好像没有性能差异一样。这意味着操作系统不知道内存节点。该系统是功能性的,但性能取决于内存的分配方式。本地访问和远程访问之间的差异越小,这个选项就越可行。

这种方法允许软件和操作系统在不修改的情况下运行。通常,当首次使用具有NUMA特征的系统时,这是系统软件的初始方法。性能不会是最优的,而且可能在每次机器和/或应用程序运行时都是不同的,因为分配给性能关键段的内存取决于系统配置和启动时的计时效果。

硬件中的内存分条.有些机器可以设置从内存地址到节点中的高速缓存线的映射,这样地址空间中的连续高速缓存线从不同的内存控制器获取(在高速缓存线级别交错)。因此,NUMA效果被平均掉(因为大于缓存线的结构将在多个NUMA节点上使用缓存线)。与忽略差异的方法相比,总体系统性能更加确定,操作系统仍然不需要知道内存性能的差异,这意味着操作系统不需要NUMA支持。由于访问分散在所有可用的NUMA节点中,因此降低了节点过载的危险。

缺点是互连是经常使用的。性能永远不会达到最佳状态,因为分条意味着经常从远程NUMA节点访问缓存线。

应用程序的启发式内存放置.如果操作系统支持NUMA(在Linux下,NUMA必须在编译时启用,BIOS或固件必须提供NUMA内存信息,以便NUMA功能变为活动的;NUMA可以在运行时通过内核参数禁用和控制),那么有一些措施可以让应用程序分配最小化信号路径长度的内存,从而提高性能,这是很有用的。操作系统必须采用为尽可能多的应用程序最大化性能的策略。使用启发式方法,特别是与前面讨论的方法相比,大多数应用程序运行时性能都有所提高。

支持numa的操作系统从固件中确定内存特征,因此可以根据内存配置调优自己的内部操作。然而,这种调优需要编码工作,因此只有操作系统的性能关键部分倾向于针对NUMA亲和性进行优化,而性能不那么关键的组件倾向于在所有内存都相等的假设下继续运行。


支持numa的操作系统从固件中确定内存特征,因此可以根据内存配置调优自己的内部操作。


操作系统最常见的假设是,应用程序将运行在本地节点上,来自本地节点的内存是首选。如果可能,进程请求的所有内存将从本地节点分配,从而避免使用交叉连接。但是,如果所需处理器的数量高于套接字上可用的硬件上下文的数量(那么必须使用两个NUMA节点上的处理器),则这种方法是无效的;如果应用程序使用的内存超过节点上的可用内存;或者在发生内存分配后,应用程序程序员或调度器决定将应用程序线程移动到不同套接字上的处理器。

一般来说,小型Unix工具和小型应用程序可以很好地使用这种方法。使用大量系统总内存和系统上大多数处理器的大型应用程序通常会从利用NUMA的显式调优或软件修改中受益。

大多数unix风格的操作系统都支持这种操作模式。值得注意的是,FreeBSD和Solaris对放置内存结构进行了优化,以避免瓶颈。FreeBSD可以在多个节点上放置内存轮询,这样延迟就平均出来了。这允许FreeBSD在BIOS或硬件级别上不能进行缓存线交错的系统上更好地工作(计划为FreeBSD 10提供额外的NUMA支持)。Solaris还按本地组复制重要的内核数据结构。

应用程序的特殊NUMA配置.操作系统提供了配置选项,允许操作人员告诉操作系统,应用程序不应该按照关于内存放置的默认假设运行。可以在不修改代码的情况下为应用程序建立内存分配策略。

Linux下存在命令行工具,它们可以设置策略来确定内存亲和性(taskset, numactl)。Solaris还提供了操作系统如何从本地组分配内存的可调参数。这些与Linux的进程内存分配策略大致相当。

NUMA分配的应用控制.应用程序可能希望对操作系统如何处理其每个内存段的分配进行细粒度控制。为此目的,存在允许应用程序指定哪个内存区域应该使用哪个内存分配策略的系统调用。

主要的性能问题通常涉及大型结构,应用程序的线程经常从所有内存节点访问这些结构,而且这些结构通常包含需要在所有线程之间共享的信息。最好使用交错放置,以便对象分布在所有可用节点上。

回到顶部

Linux如何处理NUMA?

Linux在分区中管理内存。在非numa Linux系统中,分区用于描述支持不能对所有内存位置执行DMA(直接内存访问)的设备所需的内存范围。区域还用于标记其他特殊需求的内存,如可移动内存或需要显式映射才能由内核(HIGHMEM)访问的内存,但这与这里的讨论无关。当启用NUMA时,将创建更多的内存区域,并且它们也与NUMA节点相关联。NUMA节点可以有多个区域,因为它可以服务于多个DMA区域。Linux如何安排内存可以通过观察来确定/proc/zoneinfo.分区的NUMA节点关联允许内核做出涉及相对于内核的内存延迟的决策。

在启动时,Linux将通过固件提供的ACPI(高级配置和电源接口)表检测内存的组织,然后根据需要创建映射到NUMA节点和DMA区域的区域。然后从这些区域分配内存。如果一个区域的内存耗尽,那么就会发生内存回收,Linux将扫描最近最少使用的页面,试图释放一定数量的页面。中还可以看到显示各个节点/区域中内存当前状态的计数器/proc/zoneinfo.图2显示区域/节点中的内存类型。

记忆策略.在NUMA下如何分配内存由内存策略决定。可以为进程地址空间中的内存范围指定策略,也可以为进程或整个系统指定策略。进程的策略覆盖系统策略,特定内存范围的策略覆盖进程的策略。

最重要的内存策略是:

节点本地。分配发生在本地内存节点到当前执行进程代码的位置。

交错。分配发生循环。首先从节点0分配页面,然后从节点1分配页面,然后从节点0分配页面,以此类推。交错用于分配可能从系统中的多个处理器访问的结构的内存访问,以便在互连和每个节点的内存上有均匀的负载。

还有一些在特殊情况下使用的内存策略,为了简洁起见,这里就不提了。刚才提到的两个策略通常是最有用的,操作系统默认使用它们。NODE LOCAL是系统启动和运行时的默认分配策略。

Linux内核在启动时默认使用INTERLEAVE策略。在引导过程中创建的内核结构分布在所有可用的节点上,以避免在以后进程需要访问操作系统结构时将过多的负载放在单个内存节点上。当第一个用户空间进程(初始化启动守护进程)。

通过确定进程,可以看到进程中所有内存段的活动内存分配策略(以及显示从哪个节点实际分配了多少内存的信息)id然后看内容/proc/< pid > / numa_maps。

进程启动的基本操作.进程从其父进程继承内存策略。大多数情况下,该策略保留默认值,即NODE LOCAL。当一个进程在处理器上启动时,从本地NUMA节点为该进程分配内存。进程的所有其他分配(通过增加堆、页面错误、mmap等)也将从本地NUMA节点得到满足。

Linux调度器将尝试在负载平衡期间保持进程缓存热。这意味着调度器的优先选择是将进程放在共享l1 -处理器缓存的处理器上,然后放在共享L2的处理器上,然后放在与进程最后运行的处理器共享L3的处理器上。如果超出此范围,则调度器将把进程移动到同一NUMA节点上的任何其他处理器上。

作为最后的手段,调度程序将把进程移动到另一个NUMA节点。此时,代码将在一个节点的处理器上执行,而在移动之前分配的内存将分配到旧节点上。然后,来自进程的大多数内存访问将是远程的,这将导致进程的性能下降。

最近已经进行了一些工作,使调度器支持numa,以确保进程的页面可以移回本地节点,但这项工作只在Linux 3.8及更高版本中可用,而且被认为还不成熟。关于事态的进一步信息可以在Linux内核邮件列表和文章中找到http://lwn.net

回收.Linux通常分配所有可用内存,以便缓存以后可能再次使用的数据。当内存开始不足时,将使用回收来查找没有使用或不太可能很快使用的页面。从内存中清除页面和在需要时取回页面所需的工作量因页面类型而异。Linux更喜欢从没有映射到任何进程空间的磁盘中删除页面,因为很容易删除对页面的所有引用。如果以后需要,可以从磁盘重新读取该页。映射到进程地址空间的页面需要首先从该地址空间删除页面,然后才能重用该页面。如果某个页不是磁盘上某个页的副本(匿名页),则只有在将该页首先写入交换空间(开销很大的操作)时才能删除该页。还有一些页面根本不能被删除,例如mlock ()内核数据使用的内存或页面。

因此,回收对系统的影响是不同的。在NUMA系统中,每个节点将分配多种类型的内存。每个节点上的当前空闲空间量将有所不同。因此,如果有一个内存请求,本地节点需要回收,但另一个节点有足够的内存来满足请求而不回收,那么内核有两个选择:

  • 在本地节点上运行回收传递(导致内核处理开销),然后将节点本地内存分配给进程。
  • 只需从另一个不需要回收通道的节点进行分配。内存将不是节点本地的,但我们避免频繁的回收传递。当所有区域的空闲内存都很低时,将执行回收。这种方法减少了回收的频率,并允许在一次传递中完成更多的回收工作。

对于小型NUMA系统(例如典型的双节点服务器),内核默认采用第二种方法。对于较大的NUMA系统(4个节点或更高节点),内核将尽可能执行回收以获得节点本地内存,因为延迟对进程性能的影响更大。

内核中有一个旋钮,它决定如何处理该情况/proc/sys/vm/zone_reclaim。值为0意味着不应该发生本地回收。值为1告诉内核应该运行一个回收通道,以避免来自其他节点的分配。在启动时,根据系统中最大的NUMA距离选择模式。

如果区域回收被打开,那么内核仍然试图保持回收通道尽可能轻量级。默认情况下,回收将被限制为未映射的页-缓存页。回收通道的频率可以通过设置进一步降低/proc/sys/vm/min_unmapped_ratio为运行回收通道,必须包含未映射页的内存百分比。默认为1%。

通过启用脏页的回写或匿名页的交换,可以使区域回收更加积极,但在实践中,这样做通常会导致回收的严重性能问题。

基本的NUMA命令行工具.用于为进程设置NUMA执行环境的主要工具是使用,它还允许显示系统NUMA配置,以及对共享内存段的控制。可以将进程限制到一组处理器以及一组内存节点。使用例如,可用于避免节点之间的任务迁移或限制对某个节点的内存分配。注意,如果分配受到限制,内核可能需要额外的回收通道。这些情况不受区域回收模式的影响,因为分配受到内存策略对特定节点集的限制,因此内核不能简单地选择从另一个NUMA节点中选择内存。

NUMA经常使用的另一个工具是taskset。它基本上只允许将任务绑定到处理器,因此只有一个子集使用的能力。Taskset在非numa环境中大量使用,因此熟悉的结果是开发人员更喜欢使用taskset而不是使用在NUMA系统。

NUMA信息.有许多方法可以查看有关系统和当前运行的各种进程的NUMA特征的信息。一个系统的硬件NUMA配置可以通过使用来查看使用的硬件。这包括一个狭缝(系统位置信息表)的转储,它显示了访问NUMA系统中不同节点的成本。中的例子图3显示了一个有两个节点的NUMA系统。本地接入距离为10。远程访问的费用是这个系统的两倍(20)。这是惯例,但一些供应商的实践(特别是对于双机系统)只是简单地提供10和20,而不考虑内存的实际延迟差异。

Numastat是另一个工具,用于显示本地节点满足了多少分配。特别有趣的是numa_miss计数器,该计数器指示系统从不同的节点分配内存,以避免回收。这些分配也用于其他节点。计数的其余部分是有意的节点外分配。节点外内存的数量可以作为一个指南,以确定内存是如何有效地分配给系统上运行的进程的(参见图4).

中的状态文件可以看到内存是如何分配给进程的/职业/ < pid > / numa_maps(见图5).

输出显示策略的虚拟地址,然后是关于内存范围的NUMA特征的一些信息。另一次表示页面在磁盘上没有关联的文件。Nx显示各自节点上的页数。

关于在整个系统中如何使用内存的信息可以在/proc/meminfo.中的每个NUMA节点也可以获得相同的信息/ sys /设备/系统/ < X > / meminfo节点/节点。从目录中可以获得大量其他信息meminfo所在地。通过检查并向该目录中的关键文件写入值,可以压缩内存,获得距离表,并管理巨大的页面和锁定的页面。

第一次触球的政策.为进程或地址范围指定内存策略可以造成任何内存分配,这经常让新手感到困惑。内存策略指定应该发生什么系统需要为虚拟地址分配内存。进程内存空间中未被触及或为零的页没有分配给它们内存。当进程接触或写入地址时,处理器将产生硬件故障(页面错误),那里尚未有人居住。在内核处理页面错误期间,将分配页面。然后,引起错误的指令被重新启动,并能够根据需要访问内存。

因此,重要的是有效的内存策略当分配发生时。这叫做第一次触球.首次接触策略指的是,当某个进程以某种方式首次使用页面时,根据有效策略分配页面。

页面上有效的内存策略取决于分配给内存范围的内存策略或与任务关联的内存策略。如果一个页面只被一个线程使用,那么遵循哪个策略是没有歧义的。然而,页面通常由多个线程使用。它们中的任何一个都可能导致页面被分配。如果线程具有不同的内存策略,那么对于稍后也看到相同页面的进程来说,分页的分配方式可能会令人惊讶。

例如,文本段由使用相同可执行文件的所有进程共享是相当常见的。内核将使用来自文本段的页面,如果它已经在内存中,而不管在范围上设置了什么内存策略。因此,文本段中页面的第一个用户将确定它的位置。库经常在二进制程序之间共享,特别是C库将被系统上几乎所有的进程使用。因此,当第一个使用C库的二进制程序运行时,在引导期间分配了许多最常用的页面。此时,页面将在特定的NUMA节点上建立,并在系统运行期间一直停留在那里。

首次接触现象限制了进程对其数据的位置控制。如果到文本段的距离对进程性能有重大影响,则必须在内存中移动错位的页面。内存看起来像是在当前任务的内存策略不允许的NUMA节点上分配的,因为前面的任务已经将数据放入内存。

移动内存.Linux有移动内存的能力。这意味着进程空间中内存的虚拟地址保持不变。只有数据的物理位置被移动到不同的节点。这种效果可以通过观察观察到/proc/< pid > / numa_maps搬家前后。

将一个进程的所有内存迁移到一个节点上,可以避免系统在其他NUMA节点上放置页面时的交叉连接访问,从而优化应用程序的性能。但是,普通用户只能移动仅由该进程引用的进程的页面(否则,该用户可能会干扰其他用户拥有的进程的性能优化)。只有能够移动流程的所有页面。

很难确保所有页面都是进程本地的,因为一些文本段是大量共享的,并且只能有一个页面支持一个文本段的地址。对于C库或其他高度共享的库来说,这尤其是个问题。

Linux有一个migratepages用于手动移动页面的命令行工具pid,以及源节点和目标节点。将扫描进程的内存,查找当前分配在源节点上的页面。这些将被移动到目标节点。

NUMA调度.直到Linux 3.8, Linux调度器才知道进程中内存的页面位置。关于迁移进程的决策是基于对进程内存缓存热度的估计做出的。如果Linux调度器将进程的执行移动到另一个NUMA节点,那么该进程的性能将受到显著影响,因为它的内存现在需要通过交叉连接访问。一旦移动完成,调度器将估计进程内存在远程节点上的缓存热,并尽可能长时间地将进程留在那里。因此,希望获得最佳性能的管理员认为最好不要让Linux调度器干扰内存的放置。进程经常被固定到一组特定的处理器使用taskset,或者使用cpusets特性将应用程序隔离在NUMA节点边界内。

在Linux 3.8中,解决这种情况的第一步是通过合并一个框架,该框架将使调度器能够在某个时刻考虑页面的位置,并可能自动将页面从远程节点迁移到本地节点。但是,仍然需要进行大量的开发工作,而且现有的方法并不总是能够提高给定计算负载的性能。这是今年早些时候的情况;有关Linux内核邮件列表的更多最新信息,请参见http://vger.kernel.org或文章Linux每周新闻http://lwn.net;例如,http://lwn.net/Articles/486858/).

回到顶部

结论

NUMA支持在各种操作系统中已经存在了一段时间。Linux中的NUMA支持早在2000年初就有了,而且还在不断改进。内核NUMA支持通常会优化进程执行,而不需要用户干预,而且在大多数用例中,操作系统可以简单地在NUMA系统上运行,为典型应用程序提供不错的性能。

当操作系统提供的启发不能为最终用户提供令人满意的应用程序性能时,通过工具和内核配置的特殊NUMA配置就会发挥作用。这在高性能计算、高频交易和实时应用程序中是典型的情况,但最近这些问题在常规企业级应用程序中变得更加突出。传统上,NUMA支持需要有关应用程序和硬件的特殊知识,才能使用操作系统提供的旋钮进行适当的调优。最近的开发(特别是围绕Linux NUMA调度器的开发)指出,这些开发将使操作系统能够随着时间的推移正确地自动平衡NUMA应用程序负载。

NUMA的使用需要以可能提高的性能为指导。本地和远程内存访问之间的差异越大,NUMA位置带来的好处就越大。NUMA延迟的差异是由内存访问造成的。如果应用程序不依赖于频繁的内存访问(因为,例如,处理器缓存吸收了大部分内存操作),那么NUMA优化将不起作用。同样,对于I/ o绑定的应用程序,瓶颈通常是设备而不是内存访问。为了使用NUMA优化应用程序,需要了解硬件和软件的特性。

ACM队列的q戳相关文章
queue.acm.org

Photoshop可扩展性:保持简单
克莱姆·科尔和拉塞尔·威廉姆斯
http://queue.acm.org/detaiLcfm?id=1858330

虚拟化的成本
乌尔里希Drepper
http://queue.acm.org/detail.cfm?id=1348591

性能反模式
巴特Smaalders
http://queue.acm.org/detail.cfm?id=1117403

回到顶部

更多的阅读

布雷斯威特,R.,麦考密克,P.,冯,W.。
多核NUMA体系结构中的经验内存访问成本模型。弗吉尼亚理工大学计算机科学系,2011年

黑客,G。
NUMA在RHEL 6上的应用http://www.redhat.com/summit/2012/pdf/2012-DevDay-Lab-nUMA-Hacker.pdf

克林,。
Linux的NUMA API.Novell, 2005;http://developer.amd.com/wordpress/media/2012/10/LibNUMA-WP-fv1.pdf

Lameter C。
Linux/NUMA系统上的有效同步。冰淇淋会议,2005。Linux/NUMA系统上的有效同步。冰淇淋会议,2005年

Lameter C。
2006.远程和本地内存:Linux/NUMA系统中的内存。冰淇淋会议,2006年

李,Y.,潘迪斯,I.,穆勒,R.,拉曼,V.,洛曼,G。
numa感知算法:数据变换的例子。威斯康星大学麦迪逊分校/IBM阿尔马登研究中心,2013年

爱,R。
2004.Linux内核的开发。印第安纳波利斯:sam出版

Oracle。
内存和线程布局优化开发人员指南,2010;http://docs.oracle.com/cd/E19963-01/html/820-1691/

舒密尔,K。
面向现代架构的Unix系统:面向内核程序员的对称多处理和缓存。addison - wesley, 1994

回到顶部

作者

Christoph Lameter擅长高性能计算和高频交易技术。作为一名操作系统设计人员和开发人员,他一直在为Linux开发内存管理技术,以提高性能和减少延迟。他喜欢新技术和新思维方式,这些新技术和新思维方式会颠覆现有的行业,催生新的发展社区。

回到顶部

脚注

a.具体请参见http://docs.oracle.com/cd/E19963-01/html/820-1691/gevog.htmlhttp://docs.oracle.com/cd/E19082-01/819-2239/6n4hsf6rf/index.htmlhttp://docs.oracle.com/cd/E19082-01/819-2239/madv.so.1-1/index.html/

回到顶部

数据

F1图1。具有两个NUMA节点和八个处理器的系统。

F2图2。分区/节点中的内存类型。

F3图3。显示系统的NUMA特性。

F4图4。显示系统NUMA统计信息。

F5图5。显示系统NUMA设置和统计信息。

回到顶部


©2013 0001 - 0782/13/09 ACM

允许为个人或课堂使用部分或全部作品制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。除ACM外,本作品的其他组件的版权必须受到尊重。允许有信用的文摘。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,都需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org传真(212)869-0481。

数字图书馆是由计算机协会出版的。版权所有©2013 ACM, Inc.


没有发现记录

Baidu
map