Oh tidak! Ilmu Data Saya berkarat

Halo, Habr!



Kami membawa kepada Anda terjemahan dari penelitian menarik dari perusahaan Crowdstrike. Materi ini dikhususkan untuk penggunaan bahasa Rust di bidang Ilmu Data (dalam kaitannya dengan analisis malware) dan menunjukkan bagaimana Rust dapat bersaing di bidang ini bahkan dengan NumPy dan SciPy, belum lagi Python murni .





Selamat membaca!



Python adalah salah satu bahasa pemrograman sains data yang paling populer, dan untuk alasan yang baik. Python Package Index (PyPI) memiliki sejumlah besar pustaka sains data yang mengesankan seperti NumPy, SciPy, Natural Language Toolkit, Pandas, dan Matplotlib. Dengan banyaknya perpustakaan analitik berkualitas tinggi yang tersedia dan komunitas pengembang yang luas, Python adalah pilihan yang jelas bagi banyak ilmuwan data.



Banyak dari pustaka ini diimplementasikan dalam C dan C ++ untuk alasan kinerja, tetapi menyediakan antarmuka fungsi eksternal (FFI) atau Python bindings sehingga fungsi dapat dipanggil dari Python. Implementasi bahasa tingkat rendah ini dimaksudkan untuk mengurangi beberapa kekurangan Python yang lebih terlihat, khususnya dalam hal waktu eksekusi dan konsumsi memori. Jika Anda dapat membatasi waktu eksekusi dan konsumsi memori, skalabilitas sangat disederhanakan, yang sangat penting untuk mengurangi biaya. Jika kita dapat menulis kode berkinerja tinggi yang memecahkan masalah ilmu data, maka integrasi kode tersebut dengan Python akan menjadi keuntungan yang signifikan.



Saat bekerja di persimpangan ilmu data dan analisis malwaretidak hanya eksekusi cepat diperlukan, tetapi juga penggunaan sumber daya bersama yang efisien, sekali lagi, untuk penskalaan. Penskalaan adalah salah satu masalah utama dalam data besar, seperti menangani jutaan executable secara efisien di berbagai platform. Untuk mencapai kinerja yang baik pada prosesor modern membutuhkan paralelisme, biasanya diimplementasikan menggunakan multithreading; tetapi juga diperlukan untuk meningkatkan efisiensi eksekusi kode dan konsumsi memori. Ketika memecahkan masalah seperti itu, akan sulit untuk menyeimbangkan sumber daya dari sistem lokal, dan bahkan lebih sulit untuk menerapkan sistem multi-threaded dengan benar. Inti dari C dan C ++ adalah keamanan thread tidak disediakan. Ya, ada perpustakaan khusus platform eksternal, tetapi memastikan keamanan utas jelas merupakan tugas pengembang.



Mengurai malware pada dasarnya berbahaya. Perangkat lunak berbahaya sering memanipulasi struktur data format file dengan cara yang tidak diinginkan, sehingga melumpuhkan utilitas analitik. Jebakan yang relatif umum yang menanti kita dengan Python adalah kurangnya jenis keamanan yang baik. Python, yang dengan murah hati menerima nilai-nilai Noneketika diharapkan di tempat mereka bytearray, dapat masuk ke kekacauan lengkap, yang dapat dihindari hanya dengan mengisi kode dengan cek None. Asumsi "mengetik bebek" seperti itu sering menyebabkan crash.



Tapi ada Rust. Rust diposisikan dalam banyak cara sebagai solusi ideal untuk semua masalah potensial yang diuraikan di atas: konsumsi runtime dan memori sebanding dengan C dan C ++, dan keamanan tipe yang luas disediakan. Rust juga menyediakan fasilitas tambahan, seperti jaminan keamanan memori yang kuat dan tidak ada overhead runtime. Karena tidak ada overhead seperti itu, membuatnya lebih mudah untuk mengintegrasikan kode Rust dengan kode dari bahasa lain, khususnya Python. Pada artikel ini, kami akan melakukan tur singkat Rust untuk melihat apakah itu layak hype yang terkait dengannya.



Contoh Aplikasi untuk Ilmu Data



Ilmu data adalah bidang subjek yang sangat luas dengan banyak aspek terapan, dan tidak mungkin untuk membahas semuanya dalam satu artikel. Tugas sederhana untuk ilmu data adalah menghitung entropi informasi untuk urutan byte. Rumus umum untuk menghitung entropi dalam bit diberikan di Wikipedia :







Untuk menghitung entropi untuk variabel acak X, pertama-tama kita menghitung berapa kali setiap nilai byte yang mungkin terjadi , dan kemudian membagi angka itu dengan jumlah total elemen yang ditemui untuk menghitung probabilitas menemukan nilai tertentu , masing-masing . Kemudian kita menghitung nilai negatif dari jumlah tertimbang probabilitas dari nilai tertentu yang terjadi , serta apa yang disebut informasi sendiri.... Karena kita menghitung entropi dalam bit, ini digunakan di sini (perhatikan radix 2 untuk bit).



Mari kita coba Rust dan lihat bagaimana ia menangani perhitungan entropi versus Python murni, serta beberapa pustaka Python populer yang disebutkan di atas. Ini adalah perkiraan yang disederhanakan dari potensi kinerja ilmu data Rust; Eksperimen ini bukan kritik terhadap Python atau pustaka hebat yang dikandungnya. Dalam contoh-contoh ini, kita akan menghasilkan pustaka C kita sendiri dari kode Rust yang dapat kita impor dari Python. Semua tes dijalankan di Ubuntu 18.04.



Python murni



Mari kita mulai dengan fungsi Python murni sederhana (c entropy.py) untuk menghitung entropi bytearray, hanya menggunakan modul matematika dari perpustakaan standar. Fungsi ini tidak dioptimalkan, jadi mari kita ambil sebagai titik awal untuk modifikasi dan pengukuran kinerja.



import math
def compute_entropy_pure_python(data):
    """Compute entropy on bytearray `data`."""
    counts = [0] * 256
    entropy = 0.0
    length = len(data)
    for byte in data:
        counts[byte] += 1
    for count in counts:
        if count != 0:
            probability = float(count) / length
            entropy -= probability * math.log(probability, 2)
    return entropy


Python dengan NumPy dan SciPy



Tidak mengejutkan, SciPy menyediakan fungsi untuk menghitung entropi. Tetapi pertama-tama, kita akan menggunakan fungsi unique()dari NumPy untuk menghitung frekuensi byte. Membandingkan kinerja fungsi entropi SciPy dengan implementasi lainnya agak tidak adil, karena implementasi SciPy memiliki fungsi tambahan untuk menghitung entropi relatif (jarak Kullback-Leibler). Sekali lagi, kita akan melakukan test drive (semoga tidak terlalu lambat) untuk melihat bagaimana kinerja perpustakaan Rust yang dikompilasi yang diimpor dari Python nantinya. Kami akan tetap dengan implementasi SciPy termasuk dalam skrip kami entropy.py.



import numpy as np
from scipy.stats import entropy as scipy_entropy
def compute_entropy_scipy_numpy(data):
    """  bytearray `data`  SciPy  NumPy."""
    counts = np.bincount(bytearray(data), minlength=256)
    return scipy_entropy(counts, base=2)


Python dengan Rust



Selanjutnya, kita akan mengeksplorasi implementasi Rust kita sedikit lebih banyak, dibandingkan dengan implementasi sebelumnya, demi menjadi solid dan solid. Mari kita mulai dengan paket pustaka default yang dihasilkan dengan Cargo. Bagian berikut menunjukkan bagaimana kami memodifikasi paket Rust.



cargo new --lib rust_entropy
Cargo.toml


Kami mulai dengan file manifes wajib Cargo.tomlyang mendefinisikan paket Cargo dan menentukan nama perpustakaan rust_entropy_lib. Kami menggunakan wadah cpython publik (v0.4.1) yang tersedia dari crates.io, di Rust Package Registry. Untuk artikel ini, kami menggunakan Rust v1.42.0, rilis stabil terbaru yang tersedia pada saat penulisan.



[package] name = "rust-entropy"
version = "0.1.0"
authors = ["Nobody <nobody@nowhere.com>"] edition = "2018"
[lib] name = "rust_entropy_lib"
crate-type = ["dylib"]
[dependencies.cpython] version = "0.4.1"
features = ["extension-module"]


lib.rs



Implementasi perpustakaan Rust cukup mudah. Seperti dengan implementasi Python murni kami, kami menginisialisasi array jumlah untuk setiap nilai byte yang mungkin dan beralih ke data untuk mengisi jumlah. Untuk menyelesaikan operasi, hitung dan kembalikan jumlah negatif dari probabilitas yang dikalikan dengan probabilitas.



use cpython::{py_fn, py_module_initializer, PyResult, Python};
///    
fn compute_entropy_pure_rust(data: &[u8]) -> f64 {
    let mut counts = [0; 256];
    let mut entropy = 0_f64;
    let length = data.len() as f64;
    // collect byte counts
    for &byte in data.iter() {
        counts[usize::from(byte)] += 1;
    }
    //  
    for &count in counts.iter() {
        if count != 0 {
            let probability = f64::from(count) / length;
            entropy -= probability * probability.log2();
        }
    }
    entropy
}


Yang tersisa lib.rshanyalah mekanisme untuk memanggil fungsi Rust murni dari Python. Kami memasukkan fungsi lib.rsCPython-tuned (compute_entropy_cpython())untuk memanggil fungsi Rust "murni" kami (compute_entropy_pure_rust()). Dengan melakukannya, kami hanya mendapat manfaat dari mempertahankan implementasi Rust murni tunggal dan menyediakan pembungkus yang ramah CPython.



///  Rust    CPython 
fn compute_entropy_cpython(_: Python, data: &[u8]) -> PyResult<f64> {
    let _gil = Python::acquire_gil();
    let entropy = compute_entropy_pure_rust(data);
    Ok(entropy)
}
//   Python    Rust    CPython 
py_module_initializer!(
    librust_entropy_lib,
    initlibrust_entropy_lib,
    PyInit_rust_entropy_lib,
    |py, m | {
        m.add(py, "__doc__", "Entropy module implemented in Rust")?;
        m.add(
            py,
            "compute_entropy_cpython",
            py_fn!(py, compute_entropy_cpython(data: &[u8])
            )
        )?;
        Ok(())
    }
);


Memanggil Rust Code dari Python



Akhirnya, kami menyebut implementasi Rust dari Python (sekali lagi, dari entropy.py). Untuk melakukan ini, pertama-tama kita mengimpor perpustakaan sistem dinamis kita sendiri yang dikompilasi dari Rust. Kemudian, kita cukup memanggil fungsi pustaka yang disediakan yang sebelumnya kita tentukan saat menginisialisasi modul Python menggunakan makro py_module_initializer!dalam kode Rust kita. Pada tahap ini, kami hanya memiliki satu modul Python ( entropy.py), yang mencakup fungsi untuk memanggil semua implementasi perhitungan entropi.



import rust_entropy_lib
def compute_entropy_rust_from_python(data):
    ""  bytearray `data`   Rust."""
    return rust_entropy_lib.compute_entropy_cpython(data)


Kami sedang membangun paket Rust library di atas pada Ubuntu 18.04 menggunakan Cargo. (Tautan ini mungkin berguna bagi pengguna OS X).



cargo build --release


Setelah selesai dengan perakitan, kami mengganti nama pustaka yang dihasilkan dan menyalinnya ke direktori tempat modul Python kami, sehingga dapat diimpor dari skrip. Pustaka yang Anda buat dengan Cargo dinamai librust_entropy_lib.so, tetapi Anda harus mengganti namanya rust_entropy_lib.soagar dapat berhasil mengimpor sebagai bagian dari tes ini.



Pemeriksaan kinerja: hasil



Kami mengukur kinerja setiap implementasi fungsi menggunakan breakpoint pytest, menghitung entropi selama lebih dari 1 juta byte acak. Semua implementasi ditampilkan pada data yang sama. Benchmark (juga termasuk dalam entropy.py) ditunjukkan di bawah ini.



# ###   ###
#      w/ NumPy
NUM = 1000000
VAL = np.random.randint(0, 256, size=(NUM, ), dtype=np.uint8)
def test_pure_python(benchmark):
    """  Python."""
    benchmark(compute_entropy_pure_python, VAL)
def test_python_scipy_numpy(benchmark):
    """  Python  SciPy."""
    benchmark(compute_entropy_scipy_numpy, VAL)
def test_rust(benchmark):
    """  Rust,   Python."""
    benchmark(compute_entropy_rust_from_python, VAL)


Akhirnya, kami membuat skrip driver sederhana yang terpisah untuk setiap metode yang diperlukan untuk menghitung entropi. Berikutnya adalah skrip driver representatif untuk menguji implementasi Python murni. File tersebut berisi testdata.bin1.000.000 byte acak yang digunakan untuk menguji semua metode. Setiap metode mengulangi perhitungan 100 kali untuk membuatnya lebih mudah untuk menangkap data penggunaan memori.



import entropy
with open('testdata.bin', 'rb') as f:
    DATA = f.read()
for _ in range(100):
    entropy.compute_entropy_pure_python(DATA)


Implementasi untuk SciPy / NumPy dan Rust telah menunjukkan kinerja yang baik, dengan mudah mengalahkan implementasi Python murni yang tidak dioptimalkan lebih dari 100 kali. Versi Rust tampil hanya sedikit lebih baik daripada versi SciPy / NumPy, tetapi hasilnya mengonfirmasi harapan kami: Python murni jauh lebih lambat daripada bahasa yang dikompilasi, dan ekstensi yang ditulis dengan Rust dapat berhasil bersaing dengan rekan C mereka (mengalahkan mereka bahkan dalam kondisi seperti itu) microtesting).



Ada metode lain untuk meningkatkan produktivitas juga. Kita bisa menggunakan modul ctypesatau cffi. Anda bisa menambahkan petunjuk jenis dan menggunakan Cython untuk menghasilkan perpustakaan yang bisa Anda impor dari Python. Semua opsi ini memerlukan trade-off khusus-solusi untuk dipertimbangkan.







Kami juga mengukur penggunaan memori untuk setiap implementasi fitur menggunakan aplikasi GNU time(jangan bingung dengan perintah shell built-in time). Secara khusus, kami mengukur ukuran set residen maksimum.



Sedangkan dalam implementasi Python dan Karat murni ukuran maksimum untuk bagian ini sangat mirip, implementasi SciPy / NumPy mengkonsumsi lebih banyak memori secara signifikan untuk benchmark ini. Ini mungkin karena fitur tambahan yang dimuat ke dalam memori selama impor. Bagaimanapun, panggilan kode Rust dari Python tidak muncul untuk memperkenalkan overhead memori yang signifikan.







Ringkasan



Kami sangat terkesan dengan kinerja yang kami dapatkan saat memanggil Rust dari Python. Dalam evaluasi singkat kami, implementasi Rust mampu bersaing dalam kinerja dengan implementasi basis C dari paket SciPy dan NumPy. Karat tampaknya bagus untuk pemrosesan skala besar yang efisien.



Rust tidak hanya menunjukkan waktu pelaksanaan yang sempurna; Perlu dicatat bahwa overhead memori dalam tes ini juga minimal. Karakteristik runtime dan penggunaan memori ini tampaknya ideal untuk tujuan skalabilitas. Kinerja implementasi FFI SciPy dan NumPy C benar-benar sebanding, tetapi dengan Rust kami mendapatkan keuntungan tambahan yang tidak diberikan C dan C ++ kepada kami. Keamanan memori dan jaminan keamanan benang adalah manfaat yang sangat menarik.



Sementara C memberikan runtime yang sebanding dengan Rust, C sendiri tidak memberikan keamanan utas. Ada perpustakaan eksternal yang menyediakan fungsi ini untuk C, tetapi merupakan tanggung jawab pengembang untuk memastikan bahwa mereka digunakan dengan benar. Monitor Rust untuk masalah keselamatan ulir seperti balapan pada waktu kompilasi - berkat model kepemilikannya - dan perpustakaan standar menyediakan serangkaian mekanisme konkurensi, termasuk pipa, kunci, dan smart pointer yang dihitung referensi.



Kami tidak menganjurkan porting SciPy atau NumPy ke Rust, karena pustaka Python ini sudah dioptimalkan dengan baik dan didukung oleh komunitas pengembang yang keren. Di sisi lain, kami sangat merekomendasikan kode porting dari Python murni ke Rust yang tidak disediakan di perpustakaan berkinerja tinggi. Dalam konteks aplikasi ilmu data yang digunakan untuk analisis keamanan, Rust tampaknya menjadi alternatif yang kompetitif untuk Python, mengingat kecepatan dan jaminan keamanannya.



All Articles