มีความเร็วในการวิเคราะห์หรือข้อได้เปรียบในการใช้หน่วยความจำในการใช้ HDF5 สำหรับการจัดเก็บอาร์เรย์ขนาดใหญ่ (แทนที่จะเป็นไฟล์ไบนารีแบบแบน) หรือไม่


97

ฉันกำลังประมวลผลอาร์เรย์ 3 มิติขนาดใหญ่ซึ่งฉันมักจะต้องแบ่งส่วนด้วยวิธีต่างๆเพื่อทำการวิเคราะห์ข้อมูลที่หลากหลาย "คิวบ์" ทั่วไปสามารถมีขนาด ~ 100GB (และมีแนวโน้มที่จะใหญ่ขึ้นในอนาคต)

ดูเหมือนว่ารูปแบบไฟล์ที่แนะนำโดยทั่วไปสำหรับชุดข้อมูลขนาดใหญ่ใน python คือการใช้ HDF5 (h5py หรือ pytables) คำถามของฉันคือความเร็วหรือประโยชน์ในการใช้หน่วยความจำในการใช้ HDF5 เพื่อจัดเก็บและวิเคราะห์คิวบ์เหล่านี้ผ่านการจัดเก็บไว้ในไฟล์ไบนารีแบบแบนธรรมดาหรือไม่ HDF5 เหมาะสมกว่าสำหรับข้อมูลแบบตารางเมื่อเทียบกับอาร์เรย์ขนาดใหญ่เช่นที่ฉันกำลังทำงานอยู่หรือไม่ ฉันเห็นว่า HDF5 สามารถบีบอัดข้อมูลได้ดี แต่ฉันสนใจเรื่องความเร็วในการประมวลผลและจัดการกับหน่วยความจำล้นมากกว่า

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

ฉันได้สำรวจทั้ง pytables และ h5py แล้วและยังไม่เห็นประโยชน์จากจุดประสงค์ของฉัน


1
HDF เป็นรูปแบบไฟล์ "แบบแยกส่วน" โดยเฉลี่ยแล้วจะช่วยให้คุณอ่านข้อมูลได้เร็วขึ้นมากสำหรับชุดข้อมูลของคุณโดยพลการ memmap จะมีกรณีที่ดีที่สุดที่รวดเร็ว แต่เป็นกรณีที่แย่ที่สุดช้ามาก มีความเหมาะสมดีกว่าที่จะชุดข้อมูลเช่นเดียวกับคุณมากกว่าh5py pytablesยังh5pyไม่ได้กลับ numpy อาร์เรย์ในหน่วยความจำ แต่จะส่งคืนสิ่งที่ทำหน้าที่เหมือน แต่ไม่ได้โหลดลงในหน่วยความจำ (คล้ายกับmemmappedอาร์เรย์) ฉันกำลังเขียนคำตอบที่สมบูรณ์กว่านี้ (อาจจะยังไม่จบ) แต่หวังว่าความคิดเห็นนี้จะช่วยได้เล็กน้อยในระหว่างนี้
Joe Kington

ขอบคุณ. ฉันยอมรับว่า h5py ส่งคืนชุดข้อมูลที่คล้ายกับ memmap แต่ถ้าคุณทำชิ้นส่วนของชุดข้อมูล h5py มันจะส่งกลับอาร์เรย์จำนวนนับซึ่งฉันเชื่อว่า (?) หมายถึงข้อมูลถูกใส่ลงในหน่วยความจำโดยไม่จำเป็น memmamp จะส่งคืนมุมมองไปยัง memmap ดั้งเดิมหากเป็นไปได้ ในคำอื่น ๆ : จะช่วยให้type(cube) h5py._hl.dataset.Datasetในขณะที่ช่วยให้type(cube[0:1,:,:]) numpy.ndarray
Caleb

อย่างไรก็ตามประเด็นของคุณเกี่ยวกับเวลาอ่านหนังสือโดยเฉลี่ยนั้นน่าสนใจ
Caleb

4
หากคุณมีปัญหาคอขวด I / O ในหลาย ๆ กรณีการบีบอัดสามารถปรับปรุงประสิทธิภาพการอ่าน / เขียนได้จริง (โดยเฉพาะการใช้ไลบรารีการบีบอัดที่รวดเร็วเช่น BLOSC และ LZO) เนื่องจากจะช่วยลดแบนด์วิดท์ I / O ที่ต้องใช้โดยมีค่าใช้จ่ายของรอบ CPU เพิ่มเติม . คุณอาจต้องการดูหน้านี้ซึ่งมีข้อมูลมากมายเกี่ยวกับการเพิ่มประสิทธิภาพการอ่าน - เขียนโดยใช้ไฟล์ PyTables HDF5
ali_m

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

คำตอบ:


162

ข้อดีของ HDF5: องค์กรความยืดหยุ่นความสามารถในการทำงานร่วมกัน

ข้อดีหลักบางประการของ HDF5 คือโครงสร้างลำดับชั้น (คล้ายกับโฟลเดอร์ / ไฟล์) ข้อมูลเมตาที่เป็นทางเลือกที่จัดเก็บไว้ในแต่ละรายการและความยืดหยุ่น (เช่นการบีบอัด) โครงสร้างองค์กรและการจัดเก็บข้อมูลเมตานี้อาจฟังดูไม่สำคัญ แต่มีประโยชน์มากในทางปฏิบัติ

ข้อดีอีกอย่างของ HDF คือชุดข้อมูลสามารถมีขนาดคงที่หรือขนาดที่ยืดหยุ่นได้ ดังนั้นจึงเป็นเรื่องง่ายที่จะผนวกข้อมูลเข้ากับชุดข้อมูลขนาดใหญ่โดยไม่ต้องสร้างสำเนาใหม่ทั้งหมด

นอกจากนี้ HDF5 เป็นรูปแบบมาตรฐานที่มีไลบรารีพร้อมใช้งานสำหรับเกือบทุกภาษาดังนั้นการแชร์ข้อมูลบนดิสก์ระหว่าง Matlab, Fortran, R, C และ Python นั้นทำได้ง่ายมากเมื่อใช้ HDF (เพื่อความเป็นธรรมมันไม่ยากเกินไปกับอาร์เรย์ไบนารีขนาดใหญ่เช่นกันตราบใดที่คุณทราบถึงลำดับ C เทียบกับ F และรู้จักรูปร่างประเภท d และอื่น ๆ ของอาร์เรย์ที่เก็บไว้)

ข้อดีของ HDF สำหรับอาร์เรย์ขนาดใหญ่: I / O ที่เร็วขึ้นของชิ้นส่วนตามอำเภอใจ

เช่นเดียวกับ TL / DR:สำหรับอาร์เรย์ 3 มิติ ~ 8GB การอ่านชิ้นส่วน "เต็ม" ตามแกนใด ๆ ใช้เวลาประมาณ 20 วินาทีโดยใช้ชุดข้อมูล HDF5 แบบรวมและ 0.3 วินาที (กรณีที่ดีที่สุด) ถึงมากกว่าสามชั่วโมง (กรณีที่แย่ที่สุด) สำหรับ อาร์เรย์ memmapped ของข้อมูลเดียวกัน

นอกเหนือจากสิ่งที่ระบุไว้ข้างต้นแล้วยังมีข้อได้เปรียบอีกอย่างหนึ่งสำหรับรูปแบบข้อมูลบนดิสก์ที่ "เป็นก้อน" * เช่น HDF5: การอ่านชิ้นส่วนตามอำเภอใจ (เน้นตามอำเภอใจ) โดยทั่วไปจะเร็วกว่ามากเนื่องจากข้อมูลในดิสก์จะติดกันมากกว่า เฉลี่ย.

*(HDF5 ไม่จำเป็นต้องเป็นรูปแบบข้อมูลที่แยกh5pyเป็นก้อนรองรับการแบ่งเป็นกลุ่ม แต่ไม่จำเป็นต้องใช้อันที่จริงค่าเริ่มต้นสำหรับการสร้างชุดข้อมูลในนั้นไม่ได้เป็นแบบก้อนถ้าฉันจำได้ถูกต้อง)

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

ข้อแม้อย่างหนึ่งหากคุณมี SSD คุณอาจไม่สังเกตเห็นความแตกต่างอย่างมากในความเร็วในการอ่าน / เขียน ด้วยฮาร์ดไดรฟ์ปกติการอ่านตามลำดับจะเร็วกว่าการอ่านแบบสุ่มมาก (เช่นฮาร์ดไดรฟ์ปกติใช้seekเวลานาน) HDF ยังคงมีข้อได้เปรียบใน SSD แต่เนื่องจากคุณสมบัติอื่น ๆ (เช่นข้อมูลเมตาองค์กร ฯลฯ ) มากกว่าเนื่องจากความเร็วดิบ


ก่อนอื่นเพื่อล้างความสับสนการเข้าถึงh5pyชุดข้อมูลจะส่งคืนอ็อบเจ็กต์ที่ทำงานค่อนข้างคล้ายกับอาร์เรย์ numpy แต่จะไม่โหลดข้อมูลลงในหน่วยความจำจนกว่าจะถูกแบ่งส่วน (คล้ายกับ memmap แต่ไม่เหมือนกัน) ดูข้อมูลเพิ่มเติมได้ที่h5pyบทนำ

การแบ่งชุดข้อมูลจะโหลดข้อมูลส่วนย่อยลงในหน่วยความจำ แต่คุณคงต้องการทำอะไรบางอย่างกับมันซึ่ง ณ จุดนี้คุณจะต้องใช้มันในหน่วยความจำอยู่ดี

หากคุณไม่ต้องการที่จะทำออกจากการคำนวณหลักคุณสามารถค่อนข้างง่ายสำหรับตารางข้อมูลด้วยหรือpandas pytablesเป็นไปได้ด้วยh5py(ดีกว่าสำหรับอาร์เรย์ ND ขนาดใหญ่) แต่คุณต้องเลื่อนลงไปแตะที่ระดับล่างและจัดการการวนซ้ำด้วยตัวเอง

อย่างไรก็ตามอนาคตของการคำนวณนอกคอร์ที่ไม่เหมือนใครก็คือ Blaze ลองดูสิถ้าคุณต้องการใช้เส้นทางนั้นจริงๆ


กรณี "unchunked"

ก่อนอื่นให้พิจารณาอาร์เรย์ที่สั่งซื้อ 3D C ที่เขียนลงในดิสก์ (ฉันจะจำลองโดยการเรียกarr.ravel()และพิมพ์ผลลัพธ์เพื่อให้มองเห็นสิ่งต่างๆได้มากขึ้น):

In [1]: import numpy as np

In [2]: arr = np.arange(4*6*6).reshape(4,6,6)

In [3]: arr
Out[3]:
array([[[  0,   1,   2,   3,   4,   5],
        [  6,   7,   8,   9,  10,  11],
        [ 12,  13,  14,  15,  16,  17],
        [ 18,  19,  20,  21,  22,  23],
        [ 24,  25,  26,  27,  28,  29],
        [ 30,  31,  32,  33,  34,  35]],

       [[ 36,  37,  38,  39,  40,  41],
        [ 42,  43,  44,  45,  46,  47],
        [ 48,  49,  50,  51,  52,  53],
        [ 54,  55,  56,  57,  58,  59],
        [ 60,  61,  62,  63,  64,  65],
        [ 66,  67,  68,  69,  70,  71]],

       [[ 72,  73,  74,  75,  76,  77],
        [ 78,  79,  80,  81,  82,  83],
        [ 84,  85,  86,  87,  88,  89],
        [ 90,  91,  92,  93,  94,  95],
        [ 96,  97,  98,  99, 100, 101],
        [102, 103, 104, 105, 106, 107]],

       [[108, 109, 110, 111, 112, 113],
        [114, 115, 116, 117, 118, 119],
        [120, 121, 122, 123, 124, 125],
        [126, 127, 128, 129, 130, 131],
        [132, 133, 134, 135, 136, 137],
        [138, 139, 140, 141, 142, 143]]])

ค่าจะถูกเก็บไว้ในดิสก์ตามลำดับดังแสดงในบรรทัดที่ 4 ด้านล่าง (อย่าสนใจรายละเอียดระบบไฟล์และการแยกส่วนในขณะนี้)

In [4]: arr.ravel(order='C')
Out[4]:
array([  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,
        13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,
        26,  27,  28,  29,  30,  31,  32,  33,  34,  35,  36,  37,  38,
        39,  40,  41,  42,  43,  44,  45,  46,  47,  48,  49,  50,  51,
        52,  53,  54,  55,  56,  57,  58,  59,  60,  61,  62,  63,  64,
        65,  66,  67,  68,  69,  70,  71,  72,  73,  74,  75,  76,  77,
        78,  79,  80,  81,  82,  83,  84,  85,  86,  87,  88,  89,  90,
        91,  92,  93,  94,  95,  96,  97,  98,  99, 100, 101, 102, 103,
       104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
       117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
       130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143])

ในสถานการณ์สมมติที่ดีที่สุดลองแบ่งตามแกนแรก สังเกตว่าค่าเหล่านี้เป็นเพียง 36 ค่าแรกของอาร์เรย์ นี่จะเป็นการอ่านที่เร็วมาก ! (หนึ่งแสวงหาหนึ่งอ่าน)

In [5]: arr[0,:,:]
Out[5]:
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23],
       [24, 25, 26, 27, 28, 29],
       [30, 31, 32, 33, 34, 35]])

ในทำนองเดียวกันสไลซ์ถัดไปตามแกนแรกจะเป็น 36 ค่าถัดไป ในการอ่านชิ้นส่วนทั้งหมดตามแกนนี้เราต้องการเพียงseekการดำเนินการเดียว หากสิ่งที่เรากำลังจะอ่านคือส่วนต่างๆตามแกนนี้นี่คือโครงสร้างไฟล์ที่สมบูรณ์แบบ

อย่างไรก็ตามลองพิจารณาสถานการณ์ที่เลวร้ายที่สุด: ชิ้นส่วนตามแกนสุดท้าย

In [6]: arr[:,:,0]
Out[6]:
array([[  0,   6,  12,  18,  24,  30],
       [ 36,  42,  48,  54,  60,  66],
       [ 72,  78,  84,  90,  96, 102],
       [108, 114, 120, 126, 132, 138]])

ในการอ่านชิ้นส่วนนี้เราต้องมีการค้นหา 36 รายการและการอ่าน 36 ครั้งเนื่องจากค่าทั้งหมดถูกแยกออกจากดิสก์ ไม่มีใครอยู่ติดกัน!

สิ่งนี้อาจดูเหมือนเล็กน้อย แต่เมื่อเราไปถึงอาร์เรย์ที่ใหญ่ขึ้นและใหญ่ขึ้นจำนวนและขนาดของการseekดำเนินการจะเพิ่มขึ้นอย่างรวดเร็ว สำหรับอาร์เรย์ 3 มิติขนาดใหญ่ (~ 10Gb) ที่จัดเก็บด้วยวิธีนี้และอ่านผ่านmemmapการอ่านส่วนเต็มตามแกนที่ "แย่ที่สุด" อาจใช้เวลาหลายสิบนาทีได้อย่างง่ายดายแม้จะใช้ฮาร์ดแวร์ที่ทันสมัยก็ตาม ในขณะเดียวกันชิ้นงานตามแกนที่ดีที่สุดอาจใช้เวลาน้อยกว่าหนึ่งวินาที เพื่อความง่ายฉันจะแสดงเฉพาะส่วน "เต็ม" ตามแกนเดียว แต่สิ่งเดียวกันนี้เกิดขึ้นกับชิ้นส่วนย่อยของข้อมูลใด ๆ โดยพลการ

บังเอิญมีรูปแบบไฟล์หลายรูปแบบที่ใช้ประโยชน์จากสิ่งนี้และโดยทั่วไปแล้วจะจัดเก็บสำเนาของอาร์เรย์ 3 มิติขนาดใหญ่ไว้ในดิสก์สามชุด: หนึ่งในลำดับ C หนึ่งในลำดับ F และอีกหนึ่งรายการอยู่ตรงกลางระหว่างสอง (ตัวอย่างนี้คือรูปแบบ D3D ของ Geoprobe แม้ว่าฉันจะไม่แน่ใจว่ามีการบันทึกไว้ที่ใดก็ตาม) ใครจะสนใจว่าขนาดไฟล์สุดท้ายคือ 4TB พื้นที่เก็บข้อมูลก็ถูก! สิ่งที่บ้าเกี่ยวกับเรื่องนี้ก็คือเนื่องจากกรณีการใช้งานหลักกำลังแยกชิ้นส่วนย่อยเดียวในแต่ละทิศทางการอ่านที่คุณต้องการทำนั้นรวดเร็วมาก ได้ผลดีมาก!


กรณี "ก้อน" ที่เรียบง่าย

สมมติว่าเราจัดเก็บ "ชิ้น" 2x2x2 ของอาร์เรย์ 3 มิติเป็นบล็อกที่ต่อเนื่องกันบนดิสก์ กล่าวอีกนัยหนึ่งเช่น:

nx, ny, nz = arr.shape
slices = []
for i in range(0, nx, 2):
    for j in range(0, ny, 2):
        for k in range(0, nz, 2):
            slices.append((slice(i, i+2), slice(j, j+2), slice(k, k+2)))

chunked = np.hstack([arr[chunk].ravel() for chunk in slices])

ดังนั้นข้อมูลบนดิสก์จะมีลักษณะchunkedดังนี้:

array([  0,   1,   6,   7,  36,  37,  42,  43,   2,   3,   8,   9,  38,
        39,  44,  45,   4,   5,  10,  11,  40,  41,  46,  47,  12,  13,
        18,  19,  48,  49,  54,  55,  14,  15,  20,  21,  50,  51,  56,
        57,  16,  17,  22,  23,  52,  53,  58,  59,  24,  25,  30,  31,
        60,  61,  66,  67,  26,  27,  32,  33,  62,  63,  68,  69,  28,
        29,  34,  35,  64,  65,  70,  71,  72,  73,  78,  79, 108, 109,
       114, 115,  74,  75,  80,  81, 110, 111, 116, 117,  76,  77,  82,
        83, 112, 113, 118, 119,  84,  85,  90,  91, 120, 121, 126, 127,
        86,  87,  92,  93, 122, 123, 128, 129,  88,  89,  94,  95, 124,
       125, 130, 131,  96,  97, 102, 103, 132, 133, 138, 139,  98,  99,
       104, 105, 134, 135, 140, 141, 100, 101, 106, 107, 136, 137, 142, 143])

และเพื่อแสดงให้เห็นว่าเป็นบล็อก 2x2x2 ของarrโปรดสังเกตว่านี่คือ 8 ค่าแรกของchunked:

In [9]: arr[:2, :2, :2]
Out[9]:
array([[[ 0,  1],
        [ 6,  7]],

       [[36, 37],
        [42, 43]]])

หากต้องการอ่านเป็นชิ้น ๆ ตามแกนเราจะอ่านเป็น 6 หรือ 9 ชิ้นที่ต่อเนื่องกัน (ข้อมูลมากเป็นสองเท่าที่เราต้องการ) จากนั้นเก็บเฉพาะส่วนที่เราต้องการ นั่นเป็นกรณีที่เลวร้ายที่สุดสูงสุด 9 การค้นหาเทียบกับสูงสุด 36 การค้นหาสำหรับเวอร์ชันที่ไม่เป็นกลุ่ม (แต่กรณีที่ดีที่สุดยังคงเป็น 6 Seeks vs 1 สำหรับอาร์เรย์ memmapped) เนื่องจากการอ่านตามลำดับนั้นเร็วมากเมื่อเทียบกับการค้นหาจึงช่วยลดระยะเวลาที่ใช้ในการอ่านชุดย่อยโดยพลการลงในหน่วยความจำได้อย่างมาก อีกครั้งเอฟเฟกต์นี้จะใหญ่ขึ้นด้วยอาร์เรย์ที่ใหญ่ขึ้น

HDF5 ก้าวไปอีกไม่กี่ก้าว ไม่จำเป็นต้องจัดเก็บชิ้นส่วนให้ติดกันและถูกจัดทำดัชนีโดย B-Tree นอกจากนี้พวกเขาไม่จำเป็นต้องมีขนาดเท่ากันบนดิสก์ดังนั้นจึงสามารถใช้การบีบอัดกับแต่ละชิ้นได้


อาร์เรย์แบบก้อนที่มี h5py

ตามค่าเริ่มต้นh5pyจะไม่สร้างไฟล์ HDF แบบก้อนบนดิสก์ (ฉันคิดว่าpytablesตรงกันข้าม) อย่างไรก็ตามหากคุณระบุchunks=Trueเมื่อสร้างชุดข้อมูลคุณจะได้รับอาร์เรย์แบบก้อนบนดิสก์

เป็นตัวอย่างสั้น ๆ อย่างรวดเร็ว:

import numpy as np
import h5py

data = np.random.random((100, 100, 100))

with h5py.File('test.hdf', 'w') as outfile:
    dset = outfile.create_dataset('a_descriptive_name', data=data, chunks=True)
    dset.attrs['some key'] = 'Did you want some metadata?'

สังเกตว่าchunks=Trueจะบอกh5pyให้เลือกขนาดชิ้นให้เราโดยอัตโนมัติ หากคุณทราบข้อมูลเพิ่มเติมเกี่ยวกับกรณีการใช้งานที่พบบ่อยที่สุดของคุณคุณสามารถปรับขนาด / รูปร่างของชิ้นส่วนให้เหมาะสมโดยการระบุทูเพิลรูปร่าง (เช่น(2,2,2)ในตัวอย่างง่ายๆด้านบน) สิ่งนี้ช่วยให้คุณสามารถอ่านตามแกนใดแกนหนึ่งได้อย่างมีประสิทธิภาพมากขึ้นหรือปรับให้เหมาะสมสำหรับการอ่าน / เขียนในขนาดที่กำหนด


การเปรียบเทียบประสิทธิภาพ I / O

เพื่อเน้นประเด็นให้เปรียบเทียบการอ่านเป็นชิ้น ๆ จากชุดข้อมูล HDF5 แบบก้อนและอาร์เรย์ 3 มิติขนาดใหญ่ (~ 8GB) ที่สั่งโดย Fortran ซึ่งมีข้อมูลที่ตรงกันทั้งหมด

ฉันได้ล้างแคช OS ทั้งหมดระหว่างการรันแต่ละครั้งดังนั้นเราจึงเห็นประสิทธิภาพ "เย็น"

สำหรับไฟล์แต่ละประเภทเราจะทดสอบการอ่านแบบ x-slice "เต็ม" ตามแกนแรกและสไลซ์ z "เต็ม" ตามแกนสุดท้าย สำหรับอาร์เรย์ memmapped ที่สั่งซื้อโดย Fortran สไลซ์ "x" เป็นกรณีที่เลวร้ายที่สุดและชิ้น "z" เป็นกรณีที่ดีที่สุด

รหัสที่ใช้อยู่ในส่วนสำคัญ (รวมถึงการสร้างhdfไฟล์) ฉันไม่สามารถแบ่งปันข้อมูลที่ใช้ที่นี่ได้อย่างง่ายดาย แต่คุณสามารถจำลองโดยอาร์เรย์ของศูนย์ที่มีรูปร่างเดียวกัน ( 621, 4991, 2600)และประเภทnp.uint8.

chunked_hdf.pyลักษณะเช่นนี้

import sys
import h5py

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    f = h5py.File('/tmp/test.hdf5', 'r')
    return f['seismic_volume']

def z_slice(data):
    return data[:,:,0]

def x_slice(data):
    return data[0,:,:]

main()

memmapped_array.pyคล้ายกัน แต่มีความซับซ้อนมากกว่าในการสัมผัสเพื่อให้แน่ใจว่าชิ้นส่วนถูกโหลดลงในหน่วยความจำจริง ๆ (โดยค่าเริ่มต้นmemmappedอาร์เรย์อื่นจะถูกส่งกลับซึ่งจะไม่เป็นการเปรียบเทียบแอปเปิ้ลกับแอปเปิ้ล)

import numpy as np
import sys

def main():
    data = read()

    if sys.argv[1] == 'x':
        x_slice(data)
    elif sys.argv[1] == 'z':
        z_slice(data)

def read():
    big_binary_filename = '/data/nankai/data/Volumes/kumdep01_flipY.3dv.vol'
    shape = 621, 4991, 2600
    header_len = 3072

    data = np.memmap(filename=big_binary_filename, mode='r', offset=header_len,
                     order='F', shape=shape, dtype=np.uint8)
    return data

def z_slice(data):
    dat = np.empty(data.shape[:2], dtype=data.dtype)
    dat[:] = data[:,:,0]
    return dat

def x_slice(data):
    dat = np.empty(data.shape[1:], dtype=data.dtype)
    dat[:] = data[0,:,:]
    return dat

main()

มาดูประสิทธิภาพ HDF กันก่อน:

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py z
python chunked_hdf.py z  0.64s user 0.28s system 3% cpu 23.800 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python chunked_hdf.py x
python chunked_hdf.py x  0.12s user 0.30s system 1% cpu 21.856 total

x-slice "เต็ม" และ z-slice "เต็ม" ใช้เวลาประมาณเท่ากัน (~ 20 วินาที) เมื่อพิจารณาว่านี่เป็นอาร์เรย์ 8GB ก็ไม่เลวนัก เวลาส่วนใหญ่

และถ้าเราเปรียบเทียบสิ่งนี้กับเวลาอาร์เรย์ที่มีเมมแมป (มันเป็นคำสั่งของ Fortran: A "z-slice" เป็นกรณีที่ดีที่สุดและ "x-slice" เป็นกรณีที่แย่ที่สุด):

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py z
python memmapped_array.py z  0.07s user 0.04s system 28% cpu 0.385 total

jofer at cornbread in ~ 
$ sudo ./clear_cache.sh

jofer at cornbread in ~ 
$ time python memmapped_array.py x
python memmapped_array.py x  2.46s user 37.24s system 0% cpu 3:35:26.85 total

ใช่คุณอ่านถูกต้อง 0.3 วินาทีสำหรับทิศทางหนึ่งชิ้นและ ~ 3.5 ชั่วโมงสำหรับอีกทิศทางหนึ่ง

เวลาที่จะชิ้นใน "x" ทิศทางคือไกลนานกว่าระยะเวลาที่จะใช้เวลาในการโหลดอาร์เรย์ 8GB ทั้งหมดลงในหน่วยความจำและเลือกชิ้นที่เราต้องการ! (อีกครั้งนี่คืออาร์เรย์ที่สั่งซื้อโดย Fortran เวลาที่ตรงข้าม x / z slice จะเป็นกรณีของอาร์เรย์ที่สั่งซื้อด้วย C)

อย่างไรก็ตามหากเราต้องการแบ่งส่วนตามทิศทางกรณีที่ดีที่สุดอยู่เสมออาร์เรย์ไบนารีขนาดใหญ่บนดิสก์จะดีมาก (~ 0.3 วินาที!)

ด้วยอาร์เรย์แบบเมมแมปคุณจะติดอยู่กับความคลาดเคลื่อนของ I / O นี้ (หรือบางที anisotropy เป็นคำที่ดีกว่า) อย่างไรก็ตามด้วยชุดข้อมูล HDF แบบแยกส่วนคุณสามารถเลือกขนาดชิ้นเพื่อให้การเข้าถึงเท่ากันหรือปรับให้เหมาะสมกับกรณีการใช้งานเฉพาะ ช่วยให้คุณมีความยืดหยุ่นมากขึ้น

สรุป

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

โดยรวมแล้วลองดูว่าใช้ได้ดีกับกรณีการใช้งานของคุณหรือไม่ ฉันคิดว่าคุณอาจจะแปลกใจ


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

2
ตอบโจทย์มาก! สิ่งหนึ่งที่ไม่ได้กล่าวถึงเกี่ยวกับการแบ่งชิ้นส่วนคือผลของแคชก้อน ชุดข้อมูลที่เปิดอยู่แต่ละชุดจะมีแคชชิ้นส่วนของตัวเองขนาดเริ่มต้นคือ 1 MB ซึ่งสามารถปรับได้โดยใช้ H5Pset_chunk_cache () ในภาษา C โดยทั่วไปจะมีประโยชน์ในการพิจารณาจำนวนชิ้นที่สามารถเก็บไว้ในหน่วยความจำได้เมื่อคิดถึงรูปแบบการเข้าถึงของคุณ หากแคชของคุณสามารถเก็บไว้ได้ให้พูดว่า 8 ชิ้นและชุดข้อมูลของคุณมี 10 ชิ้นในทิศทางของการสแกนคุณจะล้มเหลวมากและประสิทธิภาพจะแย่มาก
Dana Robinson
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.