当前位置: 首页 > 科技观察

使用NumPy中的视图节省内存

时间:2023-03-15 21:12:44 科技观察

如果您正在使用Python的NumPy库,通常是因为您正在处理占用大量内存的大型数组。为了减少内存使用,您可能希望尽量减少不必要的重复。NumPy有一个内置功能,可以在许多常见情况下透明地执行此操作:内存视图。此外,此功能可防止数组被垃圾回收,从而导致更高的内存使用量。在某些情况下,它可能会导致错误,并且数据可能会以意想不到的方式发生变化。为了避免这些问题,让我们了解视图如何工作以及它们如何影响您的代码。先决条件:Python列表在查看NumPy数组和视图之前,让我们考虑一个有点类似的数据结构:Python列表。Python列表与NumPy数组一样,是连续的内存块。当你切片一个Python列表时,你会得到一个完全不同的列表,这意味着你正在分配一块新的内存:>>>frompsutilimportProcess>>>Process().memory_info().rss12247040>>>list1=[None]*1_000_000>>>Process().memory_info().rss20463616>>>list2=list1[:500_000]>>>Process().memory_info().rss24580096分片list分配了更多的内存。由于第二个列表是一个独立的副本,因此如果我们更改它不会影响第一个列表:>>>list2[0]="abc">>>print(list2[0])abc>>>print(list1[0])无请注意,复制到第二个列表中的数据是指向Python对象的指针,而不是对象本身的内容。因此,即使列表本身不同,底层对象仍然在两者之间共享。当切片NumPy数组的工作方式不同时,NumPy数组不会制作副本。由于假设您可能正在处理非常大的数组,因此许多操作不会复制数组,它们只是让您查看原始数组指向的同一连续内存块。第一个结果是切片不会分配更多内存,因为它只是原始数组的视图:>>>frompsutilimportProcess>>>importnumpyasnp>>>arr=np.arange(0,1_000_000)>>>Process()。memory_info().rss37810176>>>view=arr[:500_000]>>>Process().memory_info().rss37810176视图对象看起来像一个500,000长的int64数组,所以如果它是一个新数组,它将分配大约4MB内存。但它只是同一个原始数组的一个视图,所以不需要额外的内存。为视图对象本身分配少量内存在技术上是可行的,但除非您有很多视图对象,否则这可以忽略不计。在这种情况下,RSS(常驻内存)指标中没有出现新内存,因为Python预先分配了较大的内存块,然后用较小的Python对象填充这些块。视图导致内存泄漏使用视图的后果之一是您可能会泄漏内存而不是保存内存。这是因为视图阻止了原始数组对整个数组进行垃圾回收。假设您已经决定只需要使用一个大数组的一小部分:>>>importnumpyasnp>>>frompsutilimportProcess>>>arr=np.arange(0,100_000_000)>>>Process().memory_info()。rss830181376>>>small_slice=arr[:10]>>>delarr>>>Process().memory_info().rss830111744如果这是一个Python列表,删除原始对象将释放内存。然而,在这种情况下,即使我们没有对数组的直接引用,视图仍然有效,这意味着内存没有被释放,即使我们只对其中的一小部分感兴趣。您实际上可以通过视图访问原始数组:>>>small_slicearray([0,1,2,3,4,5,6,7,8,9])>>>small_slice.basearray([0,1,2,...,99999997,99999998,99999999])结果,只有当我们删除所有视图时,原始数组的内存才会被释放:>>>delsmall_slice>>>Process().memory_info().rss29642752其他变化使用视图的另一个后果是修改视图会更改原始数组。回想一下,对于Python列表,修改切片结果不会修改原始列表,因为新对象是一个副本:>>>l=[1,2,3]>>>ll2=l[:]>>>l2[0]=17>>>l2[17,2,3]>>>l[1,2,3]使用NumPy视图后,改变视图确实改变了原来的对象,它们都指向同一个内存地址:>>>arr=np.array([1,2,3])>>>view=arr[:]>>>view[0]=17>>>viewarray([17,2,3])>>>arrarray([17,2,3])这个结果不是我们想要的!由于某些NumPyAPI可能会根据情况返回视图或副本,因此更有可能发生意外更改。例如,某些切片结果可能不是视图:>>>arr=np.array([1,2,3])>>>arrarr2=arr[:]>>>arr2.baseisarrTrue>>>arrarr3=arr[[True,False,True]]>>>arr3.baseisarrFalse改变arr2也会改变arr,但是改变arr3不会改变arr。使用copy()进行显式复制当您不想引用原始内存时,显式复制允许您创建一个新数组。这对于防止更改很有用,并且在您不想将原始数组保留在内存中的情况下也很有用:>>>arr=np.arange(0,100_000_000)>>>Process().memory_info().rss829464576>>>small_slice=arr[:10].copy()>>>delarr>>>Process().memory_info().rss29700096>>>print(small_slice.base)None在这种情况下,删除arr释放了内存,因为small_slice是一个副本,而不是一个视图。要点:高效安全地使用视图由于各种NumPyAPI会自动返回视图,因此您在编写代码时需要考虑它们:?在文档中注明API返回的是视图、副本还是两者。?如果您想从内存中清除一个大数组,请确保它不仅没有被直接引用,而且没有引用它的视图。?如果你打算改变一个数组,确保它不会仅仅因为它实际上是一个视图而意外地改变一些其他数组。?如果不需要视图,请使用copy()方法。