生成A.I(Gemini)でコード生成(@Rust)

昨日の継続ですが、素数計算のロジックをGeminiで作成してみた

<環境>

M1 Macbook Air

————————————————————–

GeminiにRustである範囲の素数計算するコード出してといって、多少は手を入れてますが、ほぼ生成されたそのまま

use std::time::Instant;

fn is_prime(num: u64) -> bool {
    if num <= 1 {
        return false;
    }
    if num <= 3 {
        return true;
    }
    if num % 2 == 0 || num % 3 == 0 {
        return false;
    }

    let mut i = 5;
    while i * i <= num {
        if num % i == 0 || num % (i + 2) == 0 {
            return false;
        }
        i += 6;
    }

    true
}

fn find_primes(start: u64, end: u64) {
    let mut pri: Vec = Vec::new();
    let start_time = Instant::now();
    for num in start..=end {
        if is_prime(num) {
            pri.push(num);
        }
    }
    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
    println!("number of primes: {}", pri.len());
}

fn main() {
    let start = 2;
    let end = 1000_000;

    println!("{}から{}までの素数:", start, end);
    find_primes(start, end);
}

最初に気づいたことは① 素数検出ロジックは2, 3で割り算できずかつ6の倍数の±1で割り切れないことが『標準』だということ、確かに計算量はそれだけでほぼ三分の一になるよね

次に、それをマルチスレッド化、これも自動生成

use rayon::prelude::*;
use std::time::Instant;

fn is_prime(num: u64) -> bool {
    if num <= 1 {
        return false;
    }
    if num <= 3 {
        return true;
    }
    if num % 2 == 0 || num % 3 == 0 {
        return false;
    }

    let mut i = 5;
    while i * i <= num { if num % i == 0 || num % (i + 2) == 0 { return false; } i += 6; } true } fn find_primes_multithreaded(start: u64, end: u64) -> Vec {
    (start..=end)
        .into_par_iter()
        .filter(|num| is_prime(*num))
        .collect()
}

fn main() {
    let start = 2;
    let end = 1000_000;

    println!("{}から{}までの素数:", start, end);

    let start_time = Instant::now();
    let primes = find_primes_multithreaded(start, end);
    let end_time = Instant::now();

    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
    println!("number of primes: {}", primes.len());
}

② rayonとinto_par_iter()使えば、マルチスレッド化がプロセッサの数に応じて自動でiter処理を実行してくれること

ちなみに実行時間を測ってみると、

<Geminiで生成したシングルスレッド版 : release mode>
2から1000000までの素数:
Elapsed time: 37.51125ms


rayon::tierクレートのinto_par_iter()メソッドでマルチスレッド対応のiteratorが作れるということ、

<Geminiで生成したマルチスレッド版 : release mode>
2から1000000までの素数:
Elapsed time: 7.397958ms

昨日のロジックではrelease modeのバイナリで90ms程度要していたから、やはり三分の一程度高速化されていて、さらにマルチスレッド版(8CPU)でオーバーヘッドあるにしてもさらに5倍ぐらい高速化

というわけで、仕様を明確に定義できる領域であれば、生成A.I使った方が人が記述するより時間短縮して効率の良いコードが作成できるだろうということで、githubのcopilotなどのその範疇(使ったことはないけど)でしょう

逆に他の言語からのそのまま置き換え(変換)は元のコードがスマートでないと無駄な作業が発生するし、もしかしたらそれほど向いていないかもしれない

 

admin

Rustのコンパイルオプション—debug vs —releaseで作成された実行ファイルの速度差

Rustのドキュメントによれば、–debugオプションと–releaseオプションで作成されたバイナリの実行速度差は10倍から最大100倍といっていますが、実際のサンプルでどうなるかみてみた

<環境>

M1 Macbook Airとラズパイzero W

百万までの素数を求めてベクターに格納、比較対象はGolnagでの実行速度、

Golangのコードはこちら、

https://github.com/chateight/golang/blob/master/concpara/waitGroup.go

Rustのコードは省力化のためにGeminiで作成、ただしマルチスレッド周りでたくさんエラー出たからシングルスレッド化して動かしてみた、マルチスレッド対応のコードは多少残存しているけど、比較の目的にはそれほど影響ないだろうからそのまま

use std::sync::{Arc, Mutex};
use std::time::Instant;

fn main() {
    let start_time = Instant::now();
    let num = find_primes(2, 10000 * 100);
    println!("{}",num.len());
    let end_time = Instant::now();
    println!("Elapsed time: {:?}", end_time.duration_since(start_time));
}

fn find_primes(start: i32, end: i32) -> Vec<i32>{
    let counter = Arc::new(Mutex::new(start)); // Shared counter for prime candidates

    let mut value = Vec::new();
    let counter_clone = counter.clone();

    loop {
        let c = counter_clone.lock().unwrap().clone();
        if c > end {
            break;
        }

        let mut flag = true;
        for j in 2..((c as f64).sqrt() as i32 + 1) {
            if c % j == 0 {
                flag = false;
                break;
            }
        }

        if flag {
            value.push(c);
        }

        *counter_clone.lock().unwrap() += 1;
    }
    value
}

結果:素数の数は重要ではないけれども、ロジック検証用

@MacBook Air M1

<Golangでの百万までの素数計算>
78498	; 素数の数
440.783125ms ; 実行時間

<rust build>
78498
Elapsed time: 508.7235ms

<rust release>
78498
Elapsed time: 91.830167ms


@Raspberry pi zero W
<rust build>
78498
Elapsed time: 18.59699031s

<rust release>
78498
Elapsed time: 7.10051562s

–debugバイナリと–releaseバイナリの実行速度差はM1 Macだと6倍弱、ラズパイzeroだと2倍程度で予想したほどの差はないというのが実感、M1 Macとラズパイzeroの速度差は安定の数十倍

Golangはマルチスレッドにしてマルチコアを使ったつもりだけれども、Rustに比較するとアドバンテージはなさそうに見える

 

admin

 

 

Rustのクロスコンパイル環境でCrossを使う

結構以前からあるツールのようですが、DockerあるいはオープンソースのコンテナであるPodmanなどのコンテナ使ってコンテナ内にターゲットのイメージを持ってきてその中でコンパイルしてくれるツールです

<環境>

・コンパイル環境:M1 Mac

・ターゲット:Raspberry pi zero W

Dockerの場合にはroot権限でなくユーザ権限でないと使えないというし、Padmanならば最初からユーザー権限ということでPodmanをインストして使ってみました

https://podman.io/docs/installation

Podmanを使うかDockerを使うかは~/.zshrcに環境変数の指定が必要で、

https://docs.rs/crate/cross/latest

export CROSS_CONTAINER_ENGINE=podman

を指定します

 

Podmanを起動状態で、

% CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build --target arm-unknown-linux-gnueabihf

を実行すると、imageをダウンロードしてその上でコンパイルします

% CROSS_CONTAINER_OPTS="--platform linux/amd64" cross build --target arm-unknown-linux-gnueabihf

Trying to pull ghcr.io/cross-rs/arm-unknown-linux-gnueabihf:0.2.5...
Getting image source signatures
Copying blob sha256:2c7e00e2a4a7dccfad0ec62cc82e2d3c05f3a4f1d4d0acc1733e78def0556d1e

~~~途中省略~~~

Writing manifest to image destination
   Compiling hello v0.1.0 (/project)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s

このようにコンパイルできて、定番のHello Worldソースからビルドした実行ファイルをラズパイ zero Wに転送すると実行できました

なぜCROSS_CONTAINER_OPTS="--platform linux/amd64"必要なのかは、

https://github.com/cross-rs/cross/issues/1214

からの情報ですがM1上のクロスだよと指定が必要のようで、指定しないとtargetで使うimage見つからないよと言われます

 

admin

ssd1306をRustで動かす

ラズパイゼロのRustクロス環境使って、ssd1306を動かしてみる

以下のリンクのサンプルプログラムを動かしてみただけですが、

https://github.com/fmckeogh/ssd1306-raspi-examples/tree/master

クロス環境はVMware +Ubuntu(20.xx)の環境です

Rust(@Raspberry PI zero)のクロスコンパイル環境構築

Cargo.tomlはそのまま使い、examplesディレクトリのgraphics-i2c.rsをビルドしてラズパイにscpで転送して実行した結果は以下のようになります

ADCもFFTもどちらのcrateも存在しているようだから、PythonコードをRustで置き換えできそうです

 

admi

Rust(@Raspberry PI zero)のクロスコンパイル環境構築

当然ながらzeroの能力ではRustのコンパイルには時間かかるので、クロス環境が必要です。以前Golang用のDocker(QEMU環境)では上手くいかなかった、おそらくlinkerの問題なのか、ので代替え案としてIntel MacのVMware環境でのUbuntuで環境作りました。

https://www.freecodecamp.org/news/embedded-rust-programming-on-raspberry-pi-zero-w/

を参考にしています。

いくつか修正が必要だったので、そこを記述します。

・ターゲットインストールのコマンドはtargetとaddが逆になってる

$ rustup target add arm-unknown-linux-gnueabihf configはobslete

 

・~/.cargo/configを使うのは古くて(obsoleteと言われる)コンパイル通らないから、.cargo/config.tomlに入れる(以下のように)てlinker対象ファイルはパスを通すかフルパスで指定

[target.arm-unknown-linux-gnueabihf]

linker = "/rpi_tools/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc"

 

・hello worldのサンプルプログラムをcargo initで作成(sampleディレクトリに)してコンパイル

fn main() {
    println!("Hello, world! from Ubuntu compiler");
}
$ cargo build --release --target=arm-unknown-linux-gnueabihf

 

・バイナリをラズパイに転送(実行ファイルはsample/target/arm-unknown-linux-gnueabihf/release以下に存在)

$ scp release/sample pi@192.168.1.16:~/sample

 

・ラズパイで実行

$ ./sample

Hello, world! from Ubuntu compiler

 

ただし、

https://github.com/raspberrypi/tools

のページには、

tools

These toolchains are deprecated. They are outdated and easily available from other sources (e.g. direct from Ubuntu apt). e.g.

sudo apt-get install gcc-arm-linux-gnueabihf

とあるので、このやり方の方がスマートなのかもしれない。

クロスコンパイルには他にはcrossとDockerを使うやり方もありますが、それほどのアドバンテージがあるようには思えないから当面この環境かな。

 

admin

 

cargo-edit tool

rustはパッケージ管理が楽なのですが、それをさらに補強するツールがcargo-editです。

https://tkyonezu.com/開発ツール-言語/raspberry-pi-に-rust-をインストールする/

を参考にラズパイ zeroにrustインスト後にcargo-editをインスト、ツールのインストには流石にzeroなのでたっぷり時間かかって(二時間ぐらいか)完了、一度はエラー終了したので再度実行。

何が便利かというと、例えばcargo add xxxxでcrate xxxxの最新版を自動で探して(版数指定もできますが)Cargo.tomlに追加してくれます。

例えばrandを追加すると、

$ cargo add rand
    Updating crates.io index
      Adding rand v0.8.5 to dependencies

で、Cargo.tomlの[dependencies]にrandが追加されています。

 

admin

NT東京 2024

NT(Tanka Tukuttemiyo)の見学に科学技術館に行ってきましたが、ここではRustの話とワークベンチの出展について

ESP32をRustがサポートするようになったので作ってみたESP32評価ボードだそうです

 

ワークベンチが欲しくて作ってみたというもの、自分の城のようなものですが、搬入がめちゃくちゃ大変で、組み立て時間は制限時間の一時間では終わらず30分超過したとか

霧箱とかの話題はnoteに、記載してます

https://note.com/coderdojoisehara/n/n9500fcb2bc85

 

admin

 

 

 

参照でライフタイム指定が必要な場合(@Rust)

Beginning Rustは言語仕様を単に羅列するのではなく、なぜそのような仕様にする必要があるかを丁寧に説明していますが、ライフサイクル指定でもそれは同じことが言えます。

そもそも関数が返すことができる参照(リファレンス)とは、以下の二つのうちのどちらかだと、

① 静的オブジェクトの借用:以下のstr関数

② 引数の借用:以下のf関数

それ以外の関数の引数オブジェクト、関数内のローカル変数オブジェクト、関数内の一時的オブジェクトは関数から戻る時に破棄されるから参照にはなり得ない、したがってライフタイム指定が必要なケース(本来は関数の戻り時点で破棄されるオブジェクトを、戻り値の参照が破棄されるまでライフタイムを延命と同義)もある程度限定的ということになる。コードとしてライフタイム表記はスッキリ感がないし、

/* It prints:
Static object
13 12*/

fn main() {
    // borrowing of the static object
    fn str<'a>() -> &'a str {
        "Static object"
    }
    println!("{}", str());

    // borrowing of the augments
    // in this case x and y need to have different life time
    fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'a i32, bool, &'b i32) {
        (x, true, y)
    }
    let i1 = 12;
    let i2;
    {
        let j1 = 13;
        let j2;
        let r = f(&i1, &j1);
        i2 = r.0;
        j2 = r.2;
        print!("{} ", *j2);
    }
    print!("{}", *i2);
}

関数fで複数のライフタイムが必要な理由は、関数fを展開してみれば理由がわかる、つまり{}の外側にあるi1/i2と内側にあるj1/j2では本質的なライフタイムに差があるのだから、それを一律なライフタイムで規定しようとしても不可能だということを言っているだけ。

 

admin

 

 

構造体や列挙体などオブジェクト要素にCopy/Cloneが実装されてない型の複製(@Rust)

コピーや複製はRustでは、オブジェクトの型に依存してデフォルトで実装されているもの(プリミティブ型)もありますが、性能を重視しているので基本は移動です。

代入のセマンティクには以下の三種類がありますが、

① 共有:本体は共有してアクセスポインタは個別に持つやり方、GCを持つ言語(Java, C#, Golang)では標準のようです

② コピー:C++のデフォルト実装が該当

③ 移動:Rustの標準、ただしプリミティブ型(オブジェクトの長さが変化しないのでスタックを使用する型)は例外

Rustでプリミティブ型以外のヒープ領域に保存されるオブジェクトは標準は移動ですが、コピー(コピーすることに意味があれば、DBのコネクションとかコピーは無意味だし害があるだろう)はcloneを実装すれば良いのですが、例えば構造体などはデフォルトでcloneを適用しようとしても、対象オブジェクトのメンバーは対象に含まれないからコンパイルは通りません。

以下のコードはコンパイルエラーになりますが、理由はderiveは構造体という型に適用されるだけで、その要素であるVec型には適用されないから。

回避方法は、以下のソースでコメントアウトしているCloneのカスタマイズが必要になります。

  |     #[derive(Copy, Clone)]
  |              ^^^^
  |     struct S {
  |         x: Vec,
  |         ----------- this field does not implement `Copy`
fn main() {
    #[derive(Copy, Clone)]
    struct S {
        x: Vec<i32>,
    }
    /*impl Clone for S {
        fn clone(&self) -> Self {
            S { x: self.x.clone() }
        }
    }*/
    let mut s1 = S { x: vec![12] };
    let s2 = s1.clone();
    s1.x[0] += 1;
    print!("{} {}", s1.x[0], s2.x[0]);

    //
    // same as above(this field(x: Vec) does not implement `Copy`)
    // Object members are out of target, Copy/Clone is applied only to struct S/S1
    //

    struct S1 {
        x: Vec<i32>,
    }
    impl Copy for S1 {}
    impl Clone for S1 {
        fn clone(&self) -> Self {
            *self
        }
    }
}

この辺りについては、

https://doc.rust-lang.org/std/marker/trait.Copy.html

の解説を読むのが良いと思います。

 

admin

コンパイルのoptimizeオプション(@Rust)

Beginning Rustに出てくるコードですが、rustcに -Oというコンパイルオプションがありますが、以下のコードで-Oを指定しないと何回か実行するとその都度時間は変わってきますが、この程度の処理時間、

実行環境はM1 Mac 16GB/512GB、rustc 1.79.0です

5.14425ms 1.031666ms

ここでrustic -Oでコンパイルすると、

584.666µs 84ns

この数字はwhileループを丸々パス、全く処理しないでvdをクリアしているだけに見えます。

fn main() {
    use std::time::Instant;
    const SIZE: usize = 40_000;
    let start_time = Instant::now();
    let mut vd = std::collections::VecDeque::<usize>::new();
    for i in 0..SIZE {
        vd.push_back(i);
        vd.push_back(SIZE + i);
        vd.pop_front();
        vd.push_back(SIZE * 2 + i);
        vd.pop_front();
    }
    let t1 = start_time.elapsed();
    while vd.len() > 0 {
        vd.pop_front();
    }
    let t2 = start_time.elapsed();
    print!("{:?} {:?}", t1, t2 - t1);
}

で、コンパイラが手抜きをさせないように、毎回結果を使うように書き換えると、

    while vd.len() > 0 {
        sum += vd.pop_front().unwrap();
    }
    let t2 = start_time.elapsed();
    print!("{:?} {:?} {}", t1, t2 - t1, sum);
386.083µs 81.209µs 2933353333

こちらはまともそうに見えます。コンパイルオプションで処理時間が著しく異なるときは実はコードが冗長というケースもありそうだよねということです、

 

admin