¿Por qué mi programa Rust es más lento que el programa Java equivalente?

Resuelto Ben Sidhom asked hace 10 años • 1 respuestas

Estaba jugando con la serialización y deserialización binaria en Rust y noté que la deserialización binaria es varios órdenes de magnitud más lenta que con Java. Para eliminar la posibilidad de gastos generales debido, por ejemplo, a asignaciones y gastos generales, simplemente leo un flujo binario de cada programa. Cada programa lee un archivo binario en el disco que contiene un entero de 4 bytes que contiene el número de valores de entrada y un fragmento contiguo de números de coma flotante codificados en big endian IEEE 754 de 8 bytes. Aquí está la implementación de Java:

import java.io.*;

public class ReadBinary {
    public static void main(String[] args) throws Exception {
        DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream(args[0])));
        int inputLength = input.readInt();
        System.out.println("input length: " + inputLength);
        try {
            for (int i = 0; i < inputLength; i++) {
                double d = input.readDouble();
                if (i == inputLength - 1) {
                    System.out.println(d);
                }
            }
        } finally {
            input.close()
        }
    }
}

Aquí está la implementación de Rust:

use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;

fn main() {
    let args = std::env::args_os();
    let fname = args.skip(1).next().unwrap();
    let path = Path::new(&fname);
    let mut file = BufReader::new(File::open(&path).unwrap());
    let input_length: i32 = read_int(&mut file);
    for i in 0..input_length {
        let d = read_double_slow(&mut file);
        if i == input_length - 1 {
            println!("{}", d);
        }
    }
}

fn read_int<R: Read>(input: &mut R) -> i32 {
    let mut bytes = [0; std::mem::size_of::<i32>()];
    input.read_exact(&mut bytes).unwrap();
    i32::from_be_bytes(bytes)
}

fn read_double_slow<R: Read>(input: &mut R) -> f64 {
    let mut bytes = [0; std::mem::size_of::<f64>()];
    input.read_exact(&mut bytes).unwrap();
    f64::from_be_bytes(bytes)
}

Estoy generando el último valor para asegurarme de que toda la entrada realmente se lea. En mi máquina, cuando el archivo contiene (los mismos) 30 millones de dobles generados aleatoriamente, la versión Java se ejecuta en 0,8 segundos, mientras que la versión Rust se ejecuta en 40,8 segundos.

Sospechando de ineficiencias en la interpretación de bytes de Rust, lo volví a intentar con una implementación personalizada de deserialización de punto flotante. Los aspectos internos son casi exactamente los mismos que se hacen en Rust's Reader , sin los IoResultenvoltorios:

fn read_double<R : Reader>(input: &mut R, buffer: &mut [u8]) -> f64 {
    use std::mem::transmute;
    match input.read_at_least(8, buffer) {
        Ok(n) => if n > 8 { fail!("n > 8") },
        Err(e) => fail!(e)
    };
    let mut val = 0u64;
    let mut i = 8;
    while i > 0 {
        i -= 1;
        val += buffer[7-i] as u64 << i * 8;
    }
    unsafe {
        transmute::<u64, f64>(val);
    }
}

El único cambio que hice en el código anterior de Rust para que esto funcionara fue crear un segmento de 8 bytes para pasarlo y (re)utilizarlo como búfer en la read_doublefunción. Esto produjo una ganancia de rendimiento significativa, funcionando en aproximadamente 5,6 segundos en promedio. Desafortunadamente, esto sigue siendo notablemente más lento (¡y más detallado!) que la versión de Java, lo que dificulta la ampliación a conjuntos de entrada más grandes. ¿Hay algo que se pueda hacer para que esto se ejecute más rápido en Rust? Más importante aún, ¿es posible realizar estos cambios de tal manera que puedan fusionarse en la Readerimplementación predeterminada para hacer que la E/S binaria sea menos dolorosa?

Como referencia, aquí está el código que estoy usando para generar el archivo de entrada:

import java.io.*;
import java.util.Random;

public class MakeBinary {
    public static void main(String[] args) throws Exception {
        DataOutputStream output = new DataOutputStream(new BufferedOutputStream(System.out));
        int outputLength = Integer.parseInt(args[0]);
        output.writeInt(outputLength);
        Random rand = new Random();
        for (int i = 0; i < outputLength; i++) {
            output.writeDouble(rand.nextDouble() * 10 + 1);
        }
        output.flush();
    }
}

(Tenga en cuenta que generar números aleatorios y escribirlos en el disco solo lleva 3,8 segundos en mi máquina de prueba).

Ben Sidhom avatar Aug 12 '14 10:08 Ben Sidhom
Aceptado

¿Has intentado ejecutar Cargo con --release?

Cuando construyes sin optimizaciones, a menudo será más lento que en Java. Pero constrúyalo con optimizaciones ( rustc -Oo cargo --release) y debería ser mucho más rápido. Si la versión estándar aún termina siendo más lenta, es algo que debe examinarse cuidadosamente para determinar dónde está la lentitud; tal vez se esté incorporando algo que no debería ser, o no debería ser, o tal vez alguna optimización que se esperaba. no está ocurriendo.

Chris Morgan avatar Aug 12 '2014 06:08 Chris Morgan