RFC 26:GDAL块缓存改进

作者:Tamas Szekeres,甚至Rouault

联系方式:szekerest@gmail.com,even.rouault,网址:spatialys.com

状态:通过、实施

实现版本:GDAL 2.1

概要和基本原理

GDAL为从驱动程序获取的栅格块维护一个内存缓存,并确保第二次访问同一块的尝试将从缓存而不是驱动程序提供服务。这个缓存以每带方式维护,并且为每个块(或子块)的指针分配一个数组。这种方法不足以处理大型栅格尺寸(或大型虚拟栅格,即使用WMS/TMS驱动程序),这可能导致GDALRasterBand::InitBlockInfo内存不足错误,如#3224中所述

例如,具有GoogleMaps平铺的21级数据集的一个波段需要256x256像素的2097152x2097152平铺。这意味着GDAL将尝试分配一个32768x32768=10亿元素(32768=2097152/64)的数组。在32位构建上,此数组的大小为4 GB,因此根本无法分配它。在64位构建上它是8gb(即使这通常只是虚拟内存保留,但由于操作系统的过度提交机制,实际上并没有分配物理内存页)。在数据集关闭时,这意味着必须对这10亿个单元进行探索,以发现剩余的缓存块。实际上,对于RGB(或RGB a)数据集,以上所有数字必须乘以3。

在哈希集实现中,内存分配直接取决于缓存块的数量。通常,默认GDAL_CACHEMAX大小为40mb时,只能同时缓存640个256x256像素的块(对于所有数据集)。

主要概念

在块缓存的设计中,对线程安全问题的认识至关重要。在gdalrasterblock.cpp中,会维护一个静态链表,以便通过从列表中删除最旧的块来跟踪块的访问顺序并将缓存大小保持在所需的限制范围内。该链表在GDAL(受hRBMutex保护)中的所有数据集和带区之间共享,并且每个带区上的线程在读取新块时,也可能触发对该互斥对象范围内的另一个带区的GDALRasterBand::UnreferenceBlock调用。FlushBlock还将通过从数组或哈希表中删除相应的平铺来访问带级缓存的数据结构。

在GDAL 2.0中,一些与线程安全相关的问题(#3225,#3226)已经被修复,这个RFC仍然将这些场景保留为安全的。

这个RFC的变化包括从GDALRasterBand类移开访问缓存块、添加或删除它的逻辑。这是用新的GDALAbstractBandBlockCache类完成的。当前基于数组的逻辑被移到新的GDALArrayBandBlockCache类中,新的基于hashset的逻辑被移到GDALHashsetBandBlockCache中。

对于基于数组的实现,由于宿主结构(数组)的“静态”特性,从并发线程读取或写入单元格时不需要特别注意。唯一需要特别注意的是防止同时访问给定的单元(块)。例如,我们希望避免TryGetLockedBlockRef()返回由另一个线程从gdalMasterBlock::FlushCacheBlock()或Internalize()释放的块。为此,现在只能通过原子函数来访问和修改GDALRasterBlock的nRefCount成员,以增加、减少或比较和交换其值。

对于基于散列集的实现,在cpl_hash_set.h/cpl_hash_set.cpp中完成的散列集数据结构的基本实现在默认情况下不是线程安全的。因此,GDALHashsetBandBlockCache有一个专用的互斥锁来保护哈希集中的所有读取、添加和删除操作。由于在hashset mutex下执行的任何操作都不涉及从GDALRasterBlock调用任何方法,因此不会发生hRBMutex的死锁。

我们本来可以重用hRBMutex来保护散列集,但这会不必要地增加hRBMutex的争用。

默认情况下,基于数组和基于哈希表的方法之间的选择基于以下规则:如果数据集有超过100万个块,则使用基于哈希集的实现,否则使用基于数组的实现。新的GDAL_OF_ARRAY_BLOCK_ACCESS和GDAL_OF_HASHSET_BLOCK_ACCESS open标志也可以传递给gdalopenx()来覆盖此选择。GDAL_BAND_BLOCK_CACHE配置选项也可以设置为ARRAY或HASHSET。

在所有情况下,基于hashset的实现都可能是默认实现(使用具有4或8个内核的autotest/cpp/testblockcache实用程序进行的性能比较显示出不可测量的差异),但理论上,基于数组的实现提供的hRBMutex争用较少,因此在使用大量核心。由于在GDAL 2.0期间已经做了一些工作来提高可伸缩性,所以现在应该谨慎地保持在基于数组的实现上,使用中等大小的raster。

由于内存分配例程涉及线程之间的同步,因此对限制对象(GDALRasterBlock实例以及CPLHashSet的内部元素)的分配/释放数量做了一些更改,这对可伸缩性有影响。

实施

要实现添加,在GDAL代码库中进行以下更改:

  • 添加了port/cpl_hash_set.cpp/port/cpl_hash_set.h:CPLHashSetClear()函数以在一次操作中移除所有元素。

  • 端口/cplu哈希_设置.cpp/port/cplu hashu set.h:CPLHashSetRemoveDeferRehash()函数用于快速删除一个元素。也就是说,内部使用的数组的潜在大小调整将推迟到以后的操作

  • port/cpl_hash_set.cpp/port/cpl_hash_set.h:改进链接列表中的“回收”链接并避免无用的malloc()/free()。

  • 端口/cpl_atomic_ops.cpp:添加CPLAtomicCompareAndExchange()

  • gcore/gdal.h:添加默认块访问的gdal、数组块访问的gdal和哈希集块访问的gdal。

  • gcore/gdal_priv.h:GDALAbstractBandBlockCache类的定义,以及GDALArrayBandBlockCacheCreate()和GDALHashSetBandBlockCacheCreate()函数。修改GDALRasterBand、GDALDataset和GDALRasterBlock定义。

  • G核心/gdalrasterband.cpp文件:InitBlockInfo()实例化适当的带区块缓存实现。

  • gcore/gdalrasterband.cpp:AdoptBlock()、UnreferenceBlock()、FlushBlock()和TryGetLockedBlockRef()方法委托给实际的带区块缓存实现。

  • gcore/gdalrasterband.cpp:addBlockToFreList()已添加并委托给GDALAbstractBandBlockCache

  • gcore/gdalrasterblock.cpp:SafeLockBlock()替换为TakeLock()

  • gcore/gdalrasterblock.cpp:RecycleFor()方法添加到回收现有块对象以保存一些新的/delete调用(由GDALAbstractBandBlockCache::CreateBlock()使用)

  • gcore/gdalrasterblock.cpp:Internalize()或FlushCacheBlock()不再直接释放块(它们仍然释放或回收其pData成员),而是将其提供给GDALRasterBand::AddBlockToFreeList()以供层重用。

  • gcore/gdalrasterblock.cpp:DropLockForRemovalFromStorage()被添加,以避免GDALRasterBand::FlushCache()或FlushBlock()与gdalrasterblock::Internalize()或FlushCacheBlock()之间的块被竞相破坏。

  • gcore/实验室stractbandblockcache.cpp文件:已添加。包含保留全局块管理器丢弃的实例化gdalMasterBlock以供以后重用的逻辑。保存一些新的/删除呼叫。

  • gcore/gdalarraybandblockcache.cpp:主要使用现有代码的gdalarraybandblockcache类实现

  • gcore/gdalhashsetbandblockcache.cpp:新的gdalhashsetbandblockcache类实现

向后兼容性

此实现保留了与现有API的向后兼容性。对GDRARASTEP带、GDLADATA数据集和GDALRasterBlock的C++ ABI进行了修改。

性能影响

在这个RFC之后,基于数组的实现应该仍然显示出与当前实现相同的性能(随着块的循环使用,可能会稍微改进)。通过autotest/cpp/testblockcache测试确认。

文档

此更改不会影响现有用户文档。

测试

autotest/cpp/test BLOCK CACHE实用程序现在由autotest/cpp/Makefile的“quick_test”目标运行,除了基于数组的实现之外,该目标还具有GDAL_BAND_BLOCK_CACHE=HASHSET。

开发了一个新的autotest/cpp/testblockcachelimits实用程序来测试一些赛车情况。由于比赛很难触发,GDALRasterBlock的代码已经被检测到允许在特定的地方睡觉,从而能够可靠地模拟比赛。

实施

Tamas Szekeres提供了这个RFC的初始版本。它已经由Even Rouault(由 LINZ (Land Information New Zealand)

工具书类

建议的实现位于 https://github.com/rouault/gdal2/tree/rfc26_bandblockcache 储存库。

更改列表: https://github.com/rouault/gdal2/compare/rfc26_bandblockcache

相关错误:#3264,#3224。

投票历史

+1个来自Evner,DanielM,TamasS。+0来自JukkaR