Apakah ada batasan teknis atau fitur bahasa yang mencegah skrip Python saya menjadi secepat program setara C ++?


10

Saya adalah pengguna Python lama. Beberapa tahun yang lalu, saya mulai belajar C ++ untuk melihat apa yang bisa ditawarkan dalam hal kecepatan. Selama ini, saya akan terus menggunakan Python sebagai alat untuk prototyping. Tampaknya, ini adalah sistem yang baik: pengembangan lincah dengan Python, eksekusi cepat di C ++.

Baru-baru ini, saya telah menggunakan Python lagi dan lagi, dan belajar bagaimana menghindari semua jebakan dan anti-pola yang cepat saya gunakan di tahun-tahun sebelumnya dengan bahasa tersebut. Pemahaman saya bahwa menggunakan fitur tertentu (daftar pemahaman, penghitungan, dll.) Dapat meningkatkan kinerja.

Tetapi apakah ada batasan teknis atau fitur bahasa yang mencegah skrip Python saya menjadi secepat program setara C ++?


2
Ya bisa. Lihat PyPy untuk mengetahui keadaan terkini dalam kompiler Python.
Greg Hewgill

5
Semua variabel dalam python adalah polimorfik yang berarti jenis variabel hanya diketahui saat runtime. Jika Anda melihat (dengan asumsi bilangan bulat) x + y dalam bahasa mirip C, mereka melakukan penambahan bilangan bulat. Dalam python akan ada saklar pada tipe variabel pada x dan y dan kemudian fungsi penambahan yang sesuai dipilih dan kemudian akan ada pemeriksaan melimpah dan kemudian ada penambahan. Kecuali python belajar mengetik statis, overhead ini tidak akan pernah hilang.
nwp

1
@ nwp Tidak, yang itu mudah, lihat PyPy. Trickier, masih terbuka, masalah meliputi: Bagaimana mengatasi latensi start up dari kompiler JIT, bagaimana menghindari alokasi untuk grafik objek berumur panjang yang rumit, dan bagaimana memanfaatkan cache secara umum.

Jawaban:


11

Saya agak menabrak dinding ini sendiri ketika saya mengambil pekerjaan pemrograman Python penuh waktu beberapa tahun yang lalu. Saya suka Python, saya benar-benar melakukannya, tetapi ketika saya mulai melakukan beberapa tuning kinerja, saya mengalami beberapa kejutan kasar.

Pythonista yang ketat dapat mengoreksi saya, tetapi di sini ada beberapa hal yang saya temukan, dilukis dengan sapuan yang sangat luas.

  • Penggunaan memori python agak menakutkan. Python mewakili segalanya sebagai dict - yang sangat kuat, tetapi memiliki hasil yang bahkan tipe data sederhana pun raksasa. Saya ingat karakter "a" mengambil 28 byte memori. Jika Anda menggunakan struktur data besar dalam Python, pastikan untuk mengandalkan numpy atau scipy, karena mereka didukung oleh implementasi byte-array langsung.

Itu memiliki dampak kinerja, karena itu berarti ada tingkat tipuan tambahan pada saat dijalankan, selain slogging di sekitar sejumlah besar memori dibandingkan dengan bahasa lain.

  • Python memang memiliki kunci juru bahasa global, yang berarti bahwa sebagian besar, proses berjalan dengan satu utas. Mungkin ada pustaka yang mendistribusikan tugas di seluruh proses, tapi kami memutar 32 atau lebih contoh skrip python kami dan menjalankan setiap utas tunggal.

Orang lain dapat berbicara dengan model eksekusi, tetapi Python adalah kompilasi-saat-runtime dan kemudian ditafsirkan, yang berarti tidak berjalan sampai ke kode mesin. Itu juga memiliki dampak kinerja. Anda dapat dengan mudah menautkan modul C atau C ++, atau menemukannya, tetapi jika Anda hanya menjalankan Python langsung, itu akan memiliki kinerja yang baik.

Sekarang, dalam tolok ukur layanan web, Python lebih baik dibandingkan dengan bahasa kompilasi-saat-runtime lainnya seperti Ruby atau PHP. Tapi itu jauh di belakang sebagian besar bahasa yang dikompilasi. Bahkan bahasa yang mengkompilasi ke bahasa perantara dan dijalankan di VM (seperti Java atau C #) melakukan banyak hal, jauh lebih baik.

Berikut ini adalah serangkaian tes benchmark yang sangat menarik yang saya rujuk sesekali:

http://www.techempower.com/benchmarks/

(Semua yang dikatakan, aku masih sangat mencintai Python, dan jika aku mendapat kesempatan untuk memilih bahasa tempatku bekerja, itu adalah pilihan pertamaku. Seringkali, aku tidak dibatasi oleh persyaratan throughput yang gila.)


2
String "a" bukan contoh yang baik untuk titik peluru pertama. Sebuah string Java juga memiliki overhead yang cukup besar untuk string karakter tunggal, dan tetapi itu adalah overhead konstan yang diamortisasi cukup baik seiring dengan bertambahnya panjang string (satu hingga empat byte karakter tergantung pada versi, opsi build, dan konten string). Anda benar tentang objek yang ditentukan pengguna, setidaknya yang tidak digunakan __slots__. PyPy harus melakukan jauh lebih baik dalam hal ini, tetapi saya tidak cukup tahu untuk menilai.

1
Masalah kedua yang Anda tunjukkan hanya terkait dengan implementasi spesifik dan tidak melekat pada bahasa. Masalah pertama membutuhkan penjelasan: apa yang "berbobot" 28 byte bukanlah karakter itu sendiri tetapi fakta bahwa itu telah dikemas dalam kelas string, datang dengan metode dan properti itu sendiri. Merupakan karakter tunggal sebagai array byte (b'a 'literal) "hanya" berbobot 18 byte pada Python 3.3 dan saya yakin ada lebih banyak cara untuk mengoptimalkan penyimpanan karakter dalam memori jika aplikasi Anda benar-benar membutuhkannya.
Merah

C # dapat mengkompilasi secara asli (mis. Teknologi MS mendatang, Xamarin untuk iOS).
Den

13

Implementasi referensi Python adalah juru bahasa "CPython". Itu mencoba untuk menjadi cukup cepat, tetapi saat ini tidak menggunakan optimisasi lanjutan. Dan untuk banyak skenario penggunaan, ini adalah hal yang baik: Kompilasi ke beberapa kode perantara terjadi segera sebelum runtime, dan setiap kali program dijalankan kode dikompilasi lagi. Jadi waktu yang dibutuhkan untuk optimasi harus ditimbang dengan waktu yang diperoleh dengan optimasi - jika tidak ada keuntungan bersih, optimasi tidak berharga. Untuk program yang berjalan sangat lama, atau program dengan loop yang sangat ketat, menggunakan optimisasi tingkat lanjut akan bermanfaat. Namun, CPython digunakan untuk beberapa pekerjaan yang menghalangi optimasi agresif:

  • Skrip berjalan pendek, digunakan misalnya untuk tugas sysadmin. Banyak sistem operasi seperti Ubuntu membangun bagian yang baik dari infrastruktur mereka di atas Python: CPython cukup cepat untuk pekerjaan itu, tetapi sebenarnya tidak memiliki waktu mulai. Selama lebih cepat dari bash, itu bagus.

  • CPython harus memiliki semantik yang jelas, karena ini adalah implementasi referensi. Ini memungkinkan pengoptimalan sederhana seperti "mengoptimalkan implementasi foo operator" atau "kompilasi daftar pemahaman untuk bytecode lebih cepat", tetapi umumnya akan menghalangi pengoptimalan yang menghancurkan informasi, seperti fungsi inlining.

Tentu saja, ada lebih banyak implementasi Python dari pada hanya CPython:

  • Jython dibangun di atas JVM. JVM dapat menginterpretasikan atau mengkompilasi JIT dengan bytecode yang disediakan, dan memiliki optimasi yang dipandu profil. Itu menderita waktu start-up yang tinggi, dan perlu beberapa saat sampai JIT masuk.

  • PyPy adalah yang paling canggih, JITting Python VM. PyPy ditulis dalam RPython, subset terbatas dari Python. Subset ini menghapus beberapa ekspresif dari Python, tetapi memungkinkan jenis variabel apa pun untuk disimpulkan secara statis. VM yang ditulis dalam RPython kemudian dapat dipindahkan ke C, yang memberikan kinerja seperti RPython C. Namun, RPython masih lebih ekspresif daripada C, yang memungkinkan pengembangan lebih cepat dari optimasi baru. PyPy adalah contoh dari bootstrap kompiler. PyPy (bukan RPython!) Sebagian besar kompatibel dengan implementasi referensi CPython.

  • Cython (seperti RPython) adalah dialek Python yang tidak kompatibel dengan pengetikan statis. Ini juga mentranspilasikan ke kode C, dan dapat dengan mudah menghasilkan ekstensi C untuk juru bahasa CPython.

Jika Anda bersedia menerjemahkan kode Python Anda ke Cython atau RPython, maka Anda akan mendapatkan kinerja seperti C. Namun, mereka seharusnya tidak dipahami sebagai "subset dari Python", tetapi lebih sebagai "C with Pythonic syntax". Jika Anda beralih ke PyPy, kode vanilla Python Anda akan mendapatkan peningkatan kecepatan yang cukup besar, tetapi juga tidak akan dapat berinteraksi dengan ekstensi yang ditulis dalam C atau C ++.

Tetapi properti atau fitur apa yang mencegah vanilla Python mencapai tingkat kinerja seperti C, selain waktu start-up yang lama?

  • Kontributor dan pendanaan. Tidak seperti Java atau C #, tidak ada perusahaan mengemudi tunggal di belakang bahasa dengan minat untuk membuat bahasa ini menjadi yang terbaik di kelasnya. Ini membatasi pengembangan terutama untuk sukarelawan, dan hibah sesekali.

  • Mengikat terlambat dan kurangnya pengetikan statis. Python memungkinkan kita untuk menulis omong kosong seperti ini:

    import random
    
    # foo is a function that returns an empty list
    def foo(): return []
    
    # foo is a function, right?
    # this ought to be equivalent to "bar = foo"
    def bar(): return foo()
    
    # ooh, we can reassign variables to a different type – randomly
    if random.randint(0, 1):
       foo = 42
    
    print bar()
    # why does this blow up (in 50% of cases)?
    # "foo" was a function while "bar" was defined!
    # ah, the joys of late binding

    Dengan Python, variabel apa saja dapat dipindahkan kapan saja. Ini mencegah caching atau inlining; akses apa pun harus melalui variabel. Tipuan ini membebani kinerja. Tentu saja: jika kode Anda tidak melakukan hal-hal gila sehingga setiap variabel dapat diberikan tipe definitif sebelum kompilasi dan setiap variabel ditugaskan hanya sekali, maka - secara teori - model eksekusi yang lebih efisien dapat dipilih. Bahasa dengan pemikiran ini akan memberikan beberapa cara untuk menandai pengidentifikasi sebagai konstanta, dan setidaknya memungkinkan anotasi jenis opsional (“pengetikan bertahap”).

  • Model objek yang dipertanyakan. Kecuali slot digunakan, sulit untuk mengetahui bidang mana yang dimiliki suatu objek (objek Python pada dasarnya adalah tabel bidang hash). Dan bahkan begitu kita di sana, kita masih tidak tahu apa jenis bidang ini. Ini mencegah representasi objek sebagai struct yang padat, seperti halnya dalam C ++. (Tentu saja, representasi objek C ++ juga tidak ideal: karena sifatnya yang seperti struct, bahkan bidang pribadi termasuk antarmuka publik dari suatu objek.)

  • Pengumpulan sampah. Dalam banyak kasus, GC dapat dihindari sepenuhnya. C ++ memungkinkan kita untuk statis mengalokasikan benda yang hancur secara otomatis ketika lingkup saat ini yang tersisa: Type instance(args);. Sampai saat itu, objek itu hidup dan dapat dipinjamkan ke fungsi lain. Ini biasanya dilakukan melalui "pass-by-reference". Bahasa seperti Rust memungkinkan kompiler untuk memeriksa secara statis bahwa tidak ada pointer ke objek seperti itu melebihi umur objek. Skema manajemen memori ini benar-benar dapat diprediksi, sangat efisien, dan sesuai untuk sebagian besar kasus tanpa grafik objek yang rumit. Sayangnya, Python tidak dirancang dengan manajemen memori dalam pikiran. Secara teori, analisis pelarian dapat digunakan untuk menemukan kasus di mana GC dapat dihindari. Dalam prakteknya, rantai metode sederhana sepertifoo().bar().baz() harus mengalokasikan sejumlah besar objek berumur pendek di heap (GC generasi adalah salah satu cara untuk menjaga masalah ini kecil).

    Dalam kasus lain, pemrogram mungkin sudah mengetahui ukuran akhir dari beberapa objek seperti daftar. Sayangnya, Python tidak menawarkan cara untuk mengomunikasikan ini ketika membuat daftar baru. Sebagai gantinya, item baru akan didorong ke ujung, yang mungkin membutuhkan realokasi beberapa kali. Beberapa catatan:

    • Daftar ukuran tertentu dapat dibuat seperti fixed_size = [None] * size. Namun, memori untuk objek di dalam daftar itu harus dialokasikan secara terpisah. Kontras C ++, yang bisa kita lakukan std::array<Type, size> fixed_size.

    • Array paket dari tipe asli tertentu dapat dibuat dengan Python melalui arraymodul builtin. Selain itu, numpymenawarkan representasi buffer data yang efisien dengan bentuk khusus untuk tipe numerik asli.

Ringkasan

Python dirancang untuk kemudahan penggunaan, bukan untuk kinerja. Desainnya membuat membuat implementasi yang sangat efisien menjadi agak sulit. Jika programmer tidak menggunakan fitur yang bermasalah, maka kompiler yang memahami idiom yang tersisa akan dapat memancarkan kode efisien yang dapat menyaingi C dalam kinerja.


8

Iya. Masalah utama adalah bahasanya didefinisikan sebagai dinamis - yaitu, Anda tidak pernah tahu apa yang Anda lakukan sampai Anda akan melakukannya. Yang membuatnya sangat sulit untuk menghasilkan kode mesin yang efisien, karena Anda tidak tahu apa yang harus menghasilkan kode mesin untuk . Kompiler JIT dapat melakukan beberapa pekerjaan di bidang ini tetapi tidak pernah sebanding dengan C ++ karena kompiler JIT tidak bisa menghabiskan waktu dan memori berjalan, karena itu waktu dan memori yang tidak Anda habiskan untuk menjalankan program Anda, dan ada batasan keras tentang apa mereka dapat mencapai tanpa melanggar semantik bahasa dinamis.

Saya tidak akan mengklaim bahwa ini merupakan tradeoff yang tidak dapat diterima. Tapi itu mendasar untuk sifat Python bahwa implementasi nyata tidak akan pernah secepat implementasi C ++.


8

Ada tiga faktor utama yang mempengaruhi kinerja semua bahasa dinamis, beberapa lebih dari yang lain.

  1. Overhead interpretatif. Saat runtime ada beberapa jenis kode byte daripada instruksi mesin dan ada overhead tetap untuk mengeksekusi kode ini.
  2. Pengiriman overhead. Target untuk panggilan fungsi tidak diketahui sampai runtime, dan mencari tahu metode mana untuk memanggil membawa biaya.
  3. Overhead manajemen memori. Bahasa yang dinamis menyimpan hal-hal dalam objek yang harus dialokasikan dan dialokasikan, dan yang membawa kinerja overhead.

Untuk C / C ++ biaya relatif dari 3 faktor ini hampir nol. Instruksi dijalankan langsung oleh prosesor, pengiriman membutuhkan paling banyak satu atau dua tipuan, memori tumpukan tidak pernah dialokasikan kecuali Anda mengatakannya. Kode yang ditulis dengan baik dapat mendekati bahasa assembly.

Untuk C # / Java dengan kompilasi JIT dua yang pertama rendah tetapi memori sampah yang dikumpulkan memiliki biaya. Kode yang ditulis dengan baik dapat mendekati 2x C / C ++.

Untuk Python / Ruby / Perl biaya ketiga faktor ini relatif tinggi. Pikirkan 5x dibandingkan dengan C / C ++ atau lebih buruk. (*)

Ingat bahwa kode pustaka runtime mungkin ditulis dalam bahasa yang sama dengan program Anda dan memiliki batasan kinerja yang sama.


(*) Karena kompilasi Just-In_Time (JIT) diperluas ke bahasa-bahasa ini, mereka juga akan mendekati (biasanya 2x) kecepatan kode C / C ++ yang ditulis dengan baik.

Juga harus dicatat bahwa begitu kesenjangannya sempit (antara bahasa yang bersaing), maka perbedaan didominasi oleh algoritma dan detail implementasi. Kode JIT dapat mengalahkan C / C ++ dan C / C ++ dapat mengalahkan bahasa assembly karena lebih mudah untuk menulis kode yang baik.


"Ingat bahwa kode pustaka runtime mungkin ditulis dalam bahasa yang sama dengan program Anda dan memiliki batasan kinerja yang sama." dan "Untuk Python / Ruby / Perl biaya ketiga faktor ini relatif tinggi. Pikirkan 5x dibandingkan dengan C / C ++ atau lebih buruk." Sebenarnya itu tidak benar. Sebagai contoh, Hashkelas Rubinius (salah satu datastructures inti di Ruby) ditulis dalam Ruby, dan kinerjanya sebanding, kadang-kadang bahkan lebih cepat, daripada Hashkelas YARV yang ditulis dalam C. Dan salah satu alasannya adalah sebagian besar runtime Rubinius sistem ditulis dalam Ruby, sehingga mereka dapat ...
Jörg W Mittag

... misalnya digarisbawahi oleh kompiler Rubinius. Contoh ekstrem adalah Klein VM (VM metacircular untuk Self) dan Maxine VM (VM metacircular untuk Java), di mana semuanya , bahkan metode pengiriman kode, pengumpul sampah, pengalokasi memori, tipe primitif, datastructures inti dan algoritma ditulis dalam Diri atau Jawa. Dengan begitu, bahkan bagian-bagian dari VM inti dapat dimasukkan ke dalam kode pengguna, dan VM dapat mengkompilasi ulang dan mengoptimalkan kembali dirinya sendiri menggunakan umpan balik runtime dari program pengguna.
Jörg W Mittag

@ JörgWMittag: Masih benar. Rubinius memiliki JIT, dan kode JIT sering kali mengalahkan C / C ++ pada masing-masing tolok ukur. Saya tidak dapat menemukan bukti bahwa hal-hal metacircular ini melakukan banyak untuk kecepatan tanpa adanya JIT. [Lihat edit untuk kejelasan tentang JIT.]
david.pfx

1

Tetapi apakah ada batasan teknis atau fitur bahasa yang mencegah skrip Python saya menjadi secepat program setara C ++?

Tidak. Ini hanya masalah uang dan sumber daya yang dituangkan untuk membuat C ++ berjalan cepat vs. uang dan sumber daya yang dituangkan untuk membuat Python berjalan cepat.

Misalnya, ketika Self VM keluar, itu bukan hanya bahasa OO dinamis tercepat, itu adalah periode bahasa OO tercepat. Meskipun menjadi bahasa yang sangat dinamis (lebih dari Python, Ruby, PHP atau JavaScript, misalnya), itu lebih cepat daripada sebagian besar implementasi C ++ yang tersedia.

Tapi kemudian Sun membatalkan proyek Self (bahasa OO tujuan umum yang matang untuk mengembangkan sistem besar) untuk fokus pada bahasa scripting kecil untuk menu animasi di kotak atas TV (Anda mungkin pernah mendengar tentang itu, itu disebut Java), tidak ada lebih banyak dana. Pada saat yang sama, Intel, IBM, Microsoft, Sun, Metrowerks, HP et al. menghabiskan banyak uang dan sumber daya membuat C ++ cepat. Produsen CPU menambahkan fitur ke chip mereka untuk membuat C ++ lebih cepat. Sistem Operasi ditulis atau dimodifikasi untuk membuat C ++ lebih cepat. Jadi, C ++ cepat.

Saya tidak terlalu terbiasa dengan Python, saya lebih merupakan orang Ruby, jadi saya akan memberikan contoh dari Ruby: Hashkelas (setara dalam fungsi dan pentingnya dengan dictPython) dalam implementasi Rubinius Ruby ditulis dalam 100% Ruby murni; namun itu bersaing dengan baik dan kadang-kadang bahkan mengungguli Hashkelas dalam YARV yang ditulis dengan tangan-dioptimalkan C. Dan dibandingkan dengan beberapa sistem Lisp atau Smalltalk komersial (atau Self VM yang disebutkan sebelumnya), kompiler Rubinius bahkan tidak sepintar itu .

Tidak ada yang melekat pada Python yang membuatnya lambat. Ada beberapa fitur dalam prosesor dan sistem operasi saat ini yang melukai Python (mis. Memori virtual dikenal buruk untuk kinerja pengumpulan sampah). Ada fitur yang membantu C ++ tetapi tidak membantu Python (CPU modern mencoba untuk menghindari kesalahan cache, karena mereka sangat mahal. Sayangnya, menghindari kesalahan cache sulit ketika Anda memiliki OO dan polimorfisme. Sebaliknya, Anda harus mengurangi biaya cache CPU Azul Vega, yang dirancang untuk Java, melakukan hal ini.)

Jika Anda menghabiskan banyak uang, penelitian dan sumber daya untuk membuat Python cepat, seperti yang dilakukan untuk C ++, dan Anda menghabiskan banyak uang, penelitian dan sumber daya untuk membuat sistem operasi yang membuat program Python berjalan cepat seperti yang dilakukan untuk C ++ dan Anda menghabiskan banyak uang, penelitian dan sumber daya untuk membuat CPU yang membuat program Python berjalan cepat seperti yang dilakukan untuk C ++, maka tidak ada keraguan dalam pikiran saya bahwa Python dapat mencapai kinerja yang sebanding dengan C ++.

Kami telah melihat dengan ECMAScript apa yang dapat terjadi jika hanya satu pemain yang serius tentang kinerja. Dalam setahun, pada dasarnya kami mengalami peningkatan kinerja 10 kali lipat untuk semua vendor besar.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.