การปล่อยหน่วยความจำใน Python


128

ฉันมีคำถามที่เกี่ยวข้องสองสามข้อเกี่ยวกับการใช้หน่วยความจำในตัวอย่างต่อไปนี้

  1. ถ้าฉันทำงานในล่าม

    foo = ['bar' for _ in xrange(10000000)]

    80.9mbหน่วยความจำที่ใช้จริงในเครื่องของฉันจะไปถึง ฉันแล้ว

    del foo

    หน่วยความจำที่แท้จริงลงไป 30.4mbแต่เพียงเพื่อ ล่ามใช้4.4mbพื้นฐานดังนั้นข้อได้เปรียบในการไม่ปล่อย26mbหน่วยความจำไปยัง OS คืออะไร? เป็นเพราะ Python กำลัง "วางแผนล่วงหน้า" โดยคิดว่าคุณอาจใช้หน่วยความจำมากขนาดนั้นอีกครั้งหรือไม่?

  2. ทำไมถึงปล่อย50.5mbโดยเฉพาะ - จำนวนเงินที่ปล่อยตาม?

  3. มีวิธีบังคับให้ Python ปล่อยหน่วยความจำทั้งหมดที่ใช้ไป (ถ้าคุณรู้ว่าจะไม่ใช้หน่วยความจำมากขนาดนั้นอีก)?

หมายเหตุ คำถามนี้แตกต่างจากฉันจะว่างหน่วยความจำอย่างชัดเจนใน Python ได้อย่างไร เนื่องจากคำถามนี้เกี่ยวข้องกับการเพิ่มการใช้หน่วยความจำจากพื้นฐานเป็นหลักแม้ว่าล่ามจะปลดปล่อยอ็อบเจ็กต์ผ่านการรวบรวมขยะgc.collectแล้วก็ตาม(โดยใช้หรือไม่ก็ตาม)


4
เป็นที่น่าสังเกตว่าพฤติกรรมนี้ไม่ได้เฉพาะกับ Python โดยทั่วไปเป็นกรณีที่เมื่อกระบวนการปลดปล่อยหน่วยความจำที่จัดสรรฮีปบางส่วนหน่วยความจำจะไม่ถูกปล่อยกลับสู่ระบบปฏิบัติการจนกว่ากระบวนการจะตาย
NPE

คำถามของคุณถามหลายสิ่งบางอย่างเป็นเรื่องซ้ำซ้อนซึ่งบางคำถามไม่เหมาะสมสำหรับ SO ซึ่งบางคำถามอาจเป็นคำถามที่ดี คุณกำลังถามว่า Python ไม่ปล่อยหน่วยความจำหรือไม่ภายใต้สถานการณ์ที่แน่นอนว่ามันสามารถ / ไม่ได้กลไกพื้นฐานคืออะไรทำไมจึงออกแบบมาเช่นนั้นไม่ว่าจะมีวิธีแก้ปัญหาหรืออย่างอื่นทั้งหมดหรือไม่?
ยกเลิก

2
@abarnert ฉันรวมคำถามย่อยที่คล้ายกัน เพื่อตอบคำถามของคุณ: ฉันรู้ว่า Python ปล่อยหน่วยความจำบางส่วนไปยังระบบปฏิบัติการ แต่ทำไมไม่ทั้งหมดและทำไมถึงมีจำนวนเท่านี้ หากมีสถานการณ์ที่ไม่สามารถทำได้เพราะเหตุใด วิธีแก้ปัญหาเช่นกัน
Jared


@jww ฉันไม่คิดอย่างนั้น gc.collectคำถามนี้จริงๆที่เกี่ยวข้องกับหน่วยความจำทำไมกระบวนการล่ามไม่เคยปล่อยแม้หลังจากการเก็บรวบรวมขยะอย่างเต็มที่ด้วยการโทรไปยัง
Jared

คำตอบ:


86

หน่วยความจำที่จัดสรรบนฮีปอาจมีรอยน้ำสูง สิ่งนี้มีความซับซ้อนโดยการเพิ่มประสิทธิภาพภายในของ Python สำหรับการจัดสรรอ็อบเจ็กต์ขนาดเล็ก ( PyObject_Malloc) ใน 4 พูล KiB ซึ่งแบ่งประเภทสำหรับขนาดการจัดสรรที่ 8 ไบต์แบบทวีคูณ - สูงสุด 256 ไบต์ (512 ไบต์ใน 3.3) สระว่ายน้ำนั้นอยู่ในสนามกีฬา 256 KiB ดังนั้นหากใช้เพียงหนึ่งบล็อกในหนึ่งพูลเวที 256 KiB ทั้งหมดจะไม่ถูกปล่อยออกมา ใน Python 3.3 ตัวจัดสรรออบเจ็กต์ขนาดเล็กถูกเปลี่ยนไปใช้แมปหน่วยความจำแบบไม่ระบุชื่อแทนฮีปดังนั้นจึงควรทำงานได้ดีกว่าในการปล่อยหน่วยความจำ

นอกจากนี้ชนิดที่มีอยู่แล้วภายในยังคงรักษาอิสระของอ็อบเจ็กต์ที่จัดสรรไว้ก่อนหน้านี้ซึ่งอาจใช้หรือไม่ใช้ตัวจัดสรรอ็อบเจ็กต์ขนาดเล็ก intประเภทรักษา freelist PyInt_ClearFreeList()กับจัดสรรหน่วยความจำของตัวเองและการล้างมันต้องโทร gc.collectนี้สามารถเรียกทางอ้อมด้วยการทำเต็มรูปแบบ

ลองแบบนี้แล้วบอกฉันว่าคุณได้อะไร นี่คือการเชื่อมโยงสำหรับpsutil.Process.memory_info

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

เอาท์พุท:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

แก้ไข:

ฉันเปลี่ยนไปใช้การวัดที่สัมพันธ์กับขนาด VM ของกระบวนการเพื่อกำจัดผลกระทบของกระบวนการอื่น ๆ ในระบบ

รันไทม์ C (เช่น glibc, msvcrt) จะลดขนาดฮีปเมื่อพื้นที่ว่างที่อยู่ติดกันที่ด้านบนถึงเกณฑ์คงที่ไดนามิกหรือกำหนดค่าได้ ด้วย glibc คุณสามารถปรับแต่งสิ่งนี้ด้วยmallopt(M_TRIM_THRESHOLD) ด้วยเหตุนี้จึงไม่น่าแปลกใจหากฮีปหดตัวมากขึ้น - มากกว่าบล็อกที่คุณfreeมาก

ใน 3.x rangeไม่สร้างรายการดังนั้นการทดสอบด้านบนจะไม่สร้างintวัตถุ10 ล้านชิ้น แม้ว่าจะเป็นเช่นนั้นก็ตามintประเภทใน 3.x ก็คือ 2.x longซึ่งไม่ได้ใช้ฟรีลิสต์


ใช้memory_info()แทนget_memory_info()และxกำหนดไว้
Aziz Alto

คุณได้รับ 10 ^ 7 intวินาทีแม้ใน Python 3 แต่แต่ละตัวจะแทนที่ตัวแปรสุดท้ายในตัวแปรลูปดังนั้นจึงไม่มีอยู่ทั้งหมดในคราวเดียว
Davis Herring

ฉันพบปัญหาหน่วยความจำรั่วและฉันเดาว่าสาเหตุที่คุณตอบที่นี่ แต่ฉันจะพิสูจน์การเดาของฉันได้อย่างไร? มีเครื่องมือใดบ้างที่สามารถแสดงพูลจำนวนมากเป็น malloced แต่ใช้เพียงบล็อกเล็ก ๆ เท่านั้น?
ruiruige1991

130

ฉันเดาว่าคำถามที่คุณสนใจคือ:

มีวิธีบังคับให้ Python ปล่อยหน่วยความจำทั้งหมดที่ใช้ไป (ถ้าคุณรู้ว่าจะไม่ใช้หน่วยความจำมากขนาดนั้นอีก)?

ไม่มีไม่มี แต่มีวิธีแก้ปัญหาง่ายๆคือกระบวนการย่อย

หากคุณต้องการพื้นที่เก็บข้อมูลชั่วคราว 500MB เป็นเวลา 5 นาที แต่หลังจากนั้นคุณจะต้องใช้งานต่อไปอีก 2 ชั่วโมงและจะไม่แตะต้องหน่วยความจำนั้นอีกเลยให้วางกระบวนการย่อยเพื่อทำงานที่ต้องใช้หน่วยความจำมาก เมื่อกระบวนการย่อยหายไปหน่วยความจำจะถูกปล่อยออกมา

สิ่งนี้ไม่ได้เป็นเรื่องเล็กน้อยและฟรี แต่ค่อนข้างง่ายและราคาถูกซึ่งโดยปกติแล้วจะดีพอสำหรับการซื้อขายที่คุ้มค่า

ขั้นแรกวิธีที่ง่ายที่สุดในการสร้างกระบวนการลูกคือconcurrent.futures(หรือสำหรับ 3.1 และก่อนหน้านี้futuresbackport บน PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

หากคุณต้องการการควบคุมเพิ่มเติมเล็กน้อยให้ใช้multiprocessingโมดูล

ค่าใช้จ่ายคือ:

  • การเริ่มต้นกระบวนการทำงานช้าในบางแพลตฟอร์มโดยเฉพาะ Windows เรากำลังพูดถึงมิลลิวินาทีที่นี่ไม่ใช่นาทีและถ้าคุณปั่นเด็กหนึ่งคนเพื่อทำงานที่คุ้มค่า 300 วินาทีคุณจะไม่สังเกตเห็นด้วยซ้ำ แต่มันไม่ฟรี
  • หากหน่วยความจำชั่วคราวที่คุณใช้มีจำนวนมากจริงๆการทำเช่นนี้อาจทำให้โปรแกรมหลักของคุณถูกสลับออก แน่นอนว่าคุณประหยัดเวลาได้ในระยะยาวเพราะถ้าความทรงจำนั้นค้างอยู่ตลอดไปมันจะต้องนำไปสู่การแลกเปลี่ยนในบางจุด แต่สิ่งนี้สามารถเปลี่ยนความช้าทีละน้อยให้กลายเป็นความล่าช้าทั้งหมดในครั้งเดียว (และเร็วที่สุด) ที่เห็นได้ชัดเจนในบางกรณีการใช้งาน
  • การส่งข้อมูลจำนวนมากระหว่างกระบวนการอาจช้า อีกครั้งหากคุณกำลังพูดถึงการส่งอาร์กิวเมนต์มากกว่า 2K และรับผลลัพธ์ 64K กลับมาคุณจะไม่สังเกตเห็นด้วยซ้ำ แต่ถ้าคุณส่งและรับข้อมูลจำนวนมากคุณจะต้องใช้กลไกอื่น (ไฟล์mmapPed หรืออื่น ๆ API หน่วยความจำที่ใช้ร่วมกันในmultiprocessingฯลฯ )
  • การส่งข้อมูลจำนวนมากระหว่างกระบวนการหมายความว่าข้อมูลจะต้องสามารถดองได้ (หรือหากคุณติดไว้ในไฟล์หรือหน่วยความจำที่ใช้ร่วมกันก็สามารถใช้ได้structหรือเป็นไปได้ในทางที่ดีctypes)

เคล็ดลับที่ดีจริงๆแม้ว่าจะไม่สามารถแก้ปัญหาได้ :( แต่ฉันชอบมันมาก
ddofborg

32

eryksun ได้ตอบคำถาม # 1 แล้วและฉันได้ตอบคำถาม # 3 (เดิม # 4) แต่ตอนนี้เรามาตอบคำถาม # 2:

ทำไมถึงปล่อย 50.5mb โดยเฉพาะ - จำนวนเงินที่ปล่อยตาม?

สิ่งที่มันขึ้นอยู่นั้นคือความบังเอิญทั้งชุดใน Python และmallocยากที่จะคาดเดาได้

อันดับแรกขึ้นอยู่กับวิธีการวัดหน่วยความจำคุณอาจวัดได้เฉพาะหน้าที่แมปลงในหน่วยความจำเท่านั้น ในกรณีนี้เมื่อใดก็ตามที่เพจเจอร์ถูกสลับหน้าออกไปโดยเพจเจอร์หน่วยความจำจะแสดงเป็น "ว่าง" แม้ว่าจะไม่ได้รับอิสระก็ตาม

หรือคุณอาจกำลังวัดหน้าที่ใช้งานอยู่ซึ่งอาจหรือไม่นับหน้าที่จัดสรร แต่ไม่เคยสัมผัส (บนระบบที่จัดสรรมากเกินไปในแง่ดีเช่น linux) หน้าที่จัดสรร แต่ติดแท็กMADV_FREEเป็นต้น

หากคุณกำลังวัดหน้าที่จัดสรรจริงๆ (ซึ่งจริงๆแล้วไม่ใช่สิ่งที่มีประโยชน์มากนัก แต่ดูเหมือนว่าจะเป็นสิ่งที่คุณกำลังถามถึง) และเพจต่างๆได้ถูกยกเลิกการจัดสรรแล้วจริงๆสองสถานการณ์ที่อาจเกิดขึ้นได้: ไม่ว่าคุณจะ เคยใช้brkหรือเทียบเท่ากับการลดขนาดกลุ่มข้อมูล (หายากมากในปัจจุบัน) หรือคุณเคยใช้munmapหรือคล้ายกับการปล่อยกลุ่มที่แมป (นอกจากนี้ยังมีทางทฤษฎีแตกต่างเล็ก ๆ น้อย ๆ หลังในการที่จะมีวิธีการที่จะปล่อยเป็นส่วนหนึ่งของแมปส่วน-เช่นขโมยมันด้วยMAP_FIXEDสำหรับMADV_FREEส่วนที่คุณ unmap ทันที.)

แต่โปรแกรมส่วนใหญ่ไม่ได้จัดสรรสิ่งต่างๆออกจากหน้าหน่วยความจำโดยตรง พวกเขาใช้mallocตัวจัดสรรสไตล์ เมื่อคุณเรียกfreeใช้ผู้จัดสรรจะปล่อยเพจไปยังระบบปฏิบัติการได้ก็ต่อเมื่อคุณเพิ่งพบfreeอ็อบเจ็กต์สุดท้ายในการแมป (หรือใน N หน้าสุดท้ายของกลุ่มข้อมูล) ไม่มีทางที่แอปพลิเคชันของคุณสามารถคาดการณ์ได้อย่างสมเหตุสมผลหรือแม้กระทั่งตรวจพบว่าเกิดขึ้นล่วงหน้า

CPython นี้ทำให้แม้กระทั่งความซับซ้อนมากขึ้นมันมีกำหนดเอง 2 mallocระดับวัตถุจัดสรรด้านบนของการจัดสรรหน่วยความจำที่กำหนดเองที่ด้านบนของ (ดูความคิดเห็นที่มาสำหรับคำอธิบายโดยละเอียดเพิ่มเติม) และยิ่งไปกว่านั้นแม้ในระดับ C API Python น้อยกว่ามากคุณยังไม่สามารถควบคุมได้โดยตรงเมื่อมีการยกเลิกการจัดสรรออบเจ็กต์ระดับบนสุด

ดังนั้นเมื่อคุณปล่อยวัตถุคุณจะรู้ได้อย่างไรว่ามันจะปล่อยหน่วยความจำไปยัง OS หรือไม่? ก่อนอื่นคุณต้องรู้ว่าคุณได้เผยแพร่ข้อมูลอ้างอิงล่าสุดแล้ว (รวมถึงข้อมูลอ้างอิงภายในที่คุณไม่รู้จัก) เพื่อให้ GC ยกเลิกการจัดสรร (ซึ่งแตกต่างจากการใช้งานอื่น ๆ อย่างน้อย CPython จะยกเลิกการจัดสรรอ็อบเจ็กต์ทันทีที่ได้รับอนุญาต) ซึ่งโดยปกติจะยกเลิกการจัดสรรอย่างน้อยสองสิ่งในระดับถัดไปลงมา (เช่นสำหรับสตริงคุณกำลังปล่อยPyStringอ็อบเจ็กต์และสตริงบัฟเฟอร์ )

ถ้าคุณทำ deallocate วัตถุที่จะรู้ว่านี่เป็นสาเหตุที่ทำให้ลดลงในระดับถัดไป deallocate บล็อกของการจัดเก็บวัตถุที่คุณต้องรู้ว่ารัฐจัดสรรภายในของวัตถุเช่นเดียวกับวิธีการที่จะดำเนินการ (เห็นได้ชัดว่าไม่สามารถเกิดขึ้นได้เว้นแต่คุณจะยกเลิกการจัดสรรสิ่งสุดท้ายในบล็อกและถึงอย่างนั้นก็อาจไม่เกิดขึ้น)

ถ้าคุณทำ deallocate บล็อกของการจัดเก็บวัตถุที่จะรู้ว่าเรื่องนี้ทำให้เกิดการfreeโทรที่คุณต้องรู้สถานะภายในของตัวจัดสรร PyMem เช่นเดียวกับวิธีการที่จะดำเนินการ (อีกครั้งคุณต้องจัดสรรบล็อกที่ใช้งานล่าสุดภายในmallocภูมิภาค ed และถึงอย่างนั้นก็อาจไม่เกิดขึ้น)

ถ้าคุณทำ ภูมิภาคเอ็ดที่จะรู้ว่าเรื่องนี้ทำให้เกิดการหรือเทียบเท่า (หรือ), คุณต้องรู้ว่ารัฐภายในของเช่นเดียวกับวิธีการที่จะดำเนินการ และอันนี้ไม่เหมือนที่อื่นคือเฉพาะแพลตฟอร์ม (และอีกครั้งโดยทั่วไปคุณจะต้องจัดสรรการใช้งานครั้งสุดท้ายภายในกลุ่มและถึงอย่างนั้นก็อาจไม่เกิดขึ้น)freemallocmunmapbrkmallocmallocmmap

ดังนั้นหากคุณต้องการที่จะเข้าใจว่าทำไมมันถึงปล่อย 50.5mb ได้คุณจะต้องติดตามจากด้านล่างขึ้นบน เหตุใดจึงmallocยกเลิกการแมปเพจที่มีมูลค่า 50.5MB เมื่อคุณทำการfreeโทรอย่างน้อยหนึ่งครั้ง(อาจมากกว่า 50.5MB เล็กน้อย) คุณต้องอ่านแพลตฟอร์มของคุณmallocจากนั้นเดินตามตารางและรายการต่างๆเพื่อดูสถานะปัจจุบัน (ในบางแพลตฟอร์มอาจใช้ประโยชน์จากข้อมูลระดับระบบซึ่งแทบจะเป็นไปไม่ได้เลยที่จะจับภาพโดยไม่ต้องทำสแนปชอตของระบบเพื่อตรวจสอบแบบออฟไลน์ แต่โชคดีที่นี่ไม่ใช่ปัญหา) จากนั้นคุณต้อง ทำสิ่งเดียวกันใน 3 ระดับข้างต้นนั้น

ดังนั้นคำตอบเดียวที่เป็นประโยชน์สำหรับคำถามคือ "เพราะ"

เว้นแต่คุณจะทำการพัฒนาแบบ จำกัด ทรัพยากร (เช่นการพัฒนาแบบฝังตัว) คุณไม่มีเหตุผลที่จะสนใจรายละเอียดเหล่านี้

และหากคุณกำลังทำการพัฒนาแบบ จำกัด ทรัพยากรการรู้รายละเอียดเหล่านี้ก็ไร้ประโยชน์ คุณต้องทำ end-run รอบ ๆ ทุกระดับและโดยเฉพาะmmapหน่วยความจำที่คุณต้องการในระดับแอปพลิเคชัน (อาจใช้ตัวจัดสรรโซนเฉพาะแอปพลิเคชันที่เข้าใจง่ายและเข้าใจได้ดีในระหว่างนั้น)


2

ขั้นแรกคุณอาจต้องการติดตั้งการมอง:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

จากนั้นเรียกใช้ในเทอร์มินัล!

glances

ในรหัส Python ของคุณให้เพิ่มที่จุดเริ่มต้นของไฟล์ดังต่อไปนี้:

import os
import gc # Garbage Collector

หลังจากใช้ตัวแปร "Big" (เช่น myBigVar) ซึ่งคุณต้องการปล่อยหน่วยความจำให้เขียนโค้ด python ดังต่อไปนี้:

del myBigVar
gc.collect()

ในเทอร์มินัลอื่นให้เรียกใช้รหัส python ของคุณและสังเกตในเทอร์มินัล "glances" ว่าหน่วยความจำถูกจัดการอย่างไรในระบบของคุณ!

โชคดี!

ปล. ฉันคิดว่าคุณกำลังทำงานกับระบบ Debian หรือ Ubuntu

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.