Rust 에서 nightly version 에서는 benchmark 를 기본으로 제공한다. 이게 언제 stable version 에 추가 될지는 모르겠지만 rust stable 버전에서는 다음 2가지 library 가 cargo bench 명령어를 지원한다.

https://github.com/bheisler/criterion.rs

 

GitHub - bheisler/criterion.rs: Statistics-driven benchmarking library for Rust

Statistics-driven benchmarking library for Rust. Contribute to bheisler/criterion.rs development by creating an account on GitHub.

github.com

와 
https://github.com/bluss/bencher

 

GitHub - bluss/bencher: bencher is just a port of the libtest (unstable) benchmark runner to Rust stable releases. `cargo bench`

bencher is just a port of the libtest (unstable) benchmark runner to Rust stable releases. `cargo bench` on stable. "Not a better bencher!" = No feature development. Go build a better sta...

github.com

를 이용한 방법이 있다.

 

외부적으로 구현하는 방법은 매우 유사해 보인다. 

criterion.rs 는 다양한 기능을 제공하는데 비해 좀 무거워보인다. 간단히 속도 측정만을 하기에는 bencher 가 간단해 보인다. 어차피 둘다 [dev-dependencies] 에 추가하면 되기 때문에 실제 프로그램 release 시에는 해당 라이브러리를 포함하지 않는다. 

  현재 bufchr(https://github.com/yiunsr/bufchr) 이라는 rust 라이브러리를 만들고 있다. 이 라이브러리에서 benchmark 를 적용하려고 bencher 를 사용하고 있다. toml 은 다음과 같이 구성한다.

=====  https://github.com/yiunsr/bufchr/blob/0b8b02ba4a5b95b0fcdf8796e8cbcfdb18ba367a/Cargo.toml ====
[package]
name = "bufchr"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "bufchr"
path = "src/lib.rs"

[[bin]]
name = "bufchrbin"
path = "src/bin.rs"

[[bench]]
name = "bufchrbench"
harness = false
path = "benches/bench.rs"

[dev-dependencies]
bencher = "0.1.5"

==================
[[bench]] 의 path는 실제 benchmark 코드가 들어있는 위치이다. 이 코드는 각자 상황에 맞게 수정이 필요하다. 
( 디렉토리 구성은 https://github.com/yiunsr/bufchr/tree/0b8b02ba4a5b95b0fcdf8796e8cbcfdb18ba367a 을 참고하기 바란다. )

실제 benchmark 하는 코드는 아래와 같다. 
======= benches/bench.rs =======
#[macro_use]
extern crate bencher;

use bencher::Bencher;

use bufchr;

static CSV_HAYSTACK: &'static [u8] = include_bytes!("../data/gdp.csv");

fn read_gdp_csv(bench: &mut Bencher) {
    bench.iter(|| {
        let needle = b',';
        let mut bf = bufchr::Bufchr::new(CSV_HAYSTACK, needle);
        loop {
            let n = bf.next();
            if n == None{break;}
        }
    });
}

fn read_gdp_csv2(bench: &mut Bencher) {
    bench.iter(|| {
        let n1 = b',';
        let n2 = b'"';
        let mut bf = bufchr::Bufchr2::new(CSV_HAYSTACK, n1, n2);
        loop {
            let n = bf.next();
            if n == None{break;}
        }
    });
}

fn read_gdp_csv3(bench: &mut Bencher) {
    bench.iter(|| {
        let n1 = b',';
        let n2 = b'"';
        let n3 = b'\n';
        let mut bf = bufchr::Bufchr3::new(CSV_HAYSTACK, n1, n2, n3);
        loop {
            let n = bf.next();
            if n == None{break;}
        }
    });
}



benchmark_group!(benches, read_gdp_csv, read_gdp_csv2, read_gdp_csv3);
benchmark_main!(benches);
========================================

cargo bench --benches   를 통해 벤치마크를 실행할 수 있다.
실행시 아래와 같은 결과를 보여준다.
========
...................
running 3 tests
test read_gdp_csv  ... bench:       6,416 ns/iter (+/- 1,404)
test read_gdp_csv2 ... bench:      15,679 ns/iter (+/- 3,932)
test read_gdp_csv3 ... bench:      18,024 ns/iter (+/- 4,459)

test result: ok. 0 passed; 0 failed; 0 ignored; 3 measured
=============
6,416 ns/iter  이런 것의 의미는 함수 실행시간이 6416 나노 초 => 6.416 밀리 초 => 0.00614초 동안 실행했다는 의미이다. 

  며칠전 vscode 로 rust 를 디버깅하는데 안되는 case 가 있어서 stackoverflow 에 질문을 올렸다. ( yiunsr.tistory.com/859 ). 그런데, 내 설명이 부족한 것인지, 내 영어 실력이 문제인 것인지 딱히 답변이 없었다. 계속 알아보니 VSCode 의 CodeLLDM 설명에 (github.com/vadimcn/vscode-lldb/wiki/Notes-on-Windows-support) *-pc-windows-gnu 를 추천한다는 말이 있었다. MSVC 로 컴파일 할 때 뭔가 잘 안되는 경우가 있나 보다가 내가 내린 결론이다. 그러다 더 찾아보니 GDB를 이용해서 디버깅 하는 방법도 있길래 Rust ToolChain 을 stable-x86_64-pc-windows-gnu 로 변경해서 테스트 해보았다. 내 경우 사용하는 라이브러리들이 컴파일 되지 않았다. 그러다 더 생각해 보니 GDB로 되면 Microsoft Visual Studio 2017, 2019 (무료버전) 을 이용해서 디버깅 할 수 있지 않을까 생각해게 되었다. 그래서 도전해 보니 너무 잘 되는 것이었다. 더 나가 vscode 에서 msvc 디버깅을 연동하면 될 수 있을 것 같아 시도해 봤는데, 이 경우에도 너무 잘되어서 누군가에게 도움이 될 수 있도록 설명을 남겨둔다.
Visual Studio 2019 라고 제목을 적었지만 Visual Studio 2017 에서도 잘 되었다. 

 

Visual Sutdio 2019 에서 Rust 코드 디버깅하기

* 전제 조건은 컴파일은 기존대로 cargo 를 통해 디버깅 모드로 컴파일 해야 한다.
* Rust toolchain 은 stable-x86_64-pc-windows-msvc 로 컴파일 해야 한다. 
1. cargo 프로젝트 생성
 (제 경우 D:\workspace\vscode 하위에 프로젝트를 생성했습니다. 

D:\workspace\vscode>cargo new hello_world --bin

 

==== main.rs ====

1 부터 100까지의 합과 1부터 100까지의 곱을 구하는 프로그램이다. 

use std::time::Instant;

fn main() {
    println!("======== Start Program ========");
    let start = Instant::now();

    let mut sum:u64 = 0;
    for n in 1..100{
        sum = sum + n;
    }

    let mut fac:f64 = 1.0;
    for n in 1..100{
        fac = fac * (n as f64);
    }

    let duration = start.elapsed();
    println!("sum {}", sum);
    println!("factorial {}", fac);
    println!("Time elapsed in expensive_function() is: {:?}", duration);
    println!("======== End Program ========");
}

 

2. build 

build 는 기존대로 한다. 

cargo build 


3. visual studio 에서 폴더 열기
  visual sutdio 2019 실행 -> 로컬폴더 열기

hello_world 디렉토리를 선택한다. 

4. 디버깅 프로그램 선택
  Visual Sutdio 2019 는 디버깅 해야 하는 대상을 모르기 때문에 프로그램을 실행하면서 디버깅 할 수 없다. 
 솔류션 탐색기에서 target/debug/hello_world.exe 에 대해 오른쪽 마우스를 눌러서 시작항목으로 설정한다.

5. break point 설정
 src/main.rs 파일을 열고 break point 를 설정한다. 

6. 디버깅 하기
 메뉴  디버그 -> 디버그 시작   을 클릭한다. 

이렇게 하면 break point 가 걸린다. 그리고 당연히 조사식도 잘 보인다. 

 

다음에는 vscode 로 이런 것을 하는 방법에 대해 글을 남기도록 하겠다. 

 오늘 회사에서 Postgresql 의 Upsert 를 이용해서 로직을 만들었다. 이게 하나의 query 뿐만 아니라 multi로 Upsert 도 가능 한 것 같아서 query 를 효율적으로 짜기 위해 Multiple Upsert 를 구현했다. 

Upsert 는 Insert 시 뭔가 insert 를 할 수 없는 경우(특히 primary key 가 중복된다든지 Unique 조건이 있는데, 이미 Unique 한 값이 있다든지 하는 경우) 기존 row 를 업데이트 하는 쿼리를 말한다. 

CREATE TABLE test_table(
    id SERIAL PRIMARY KEY,
    name varchar,
    visit int
);

라는 test_table 을 생성한 경우 
아래 query 를 날리면

INSERT INTO test_table (id, name , visit) 
	VALUES(1, 'Bill', 1)
ON CONFLICT (id)
DO UPDATE
SET visit = test_table.visit + 1;

처음에 insert 될 때는
1, 'Bill', 1 인 상태이지만 2번째에 다시 해당 query 가 동작하면 1, 'Bill', 2   로 +1씩 visit 를 증가시키는 동작을 할 수 있다.

default 키워드


이런 동작을 할 때, Primary Key 가 꼭 필요하다. 그런데 Insert 를 할 때는 일반적으로 Primary 가 있을리가 없다. 그래서 column 자체를 갖지 않는 경우가 많은데, 프로그램에서 Query 를 이용하다보면 primary key 대신 뭔가 자리를 차지할 값을 넣어야 할 때가 있다. 이럴 때는 default 라는 키워드를 대신 사용할 수 있다.

INSERT INTO test_table (id, name , visit) VALUES(default, 'Jone', 1)

이렇게 사용할 수 있다. (이 구문은 정말 신기하다. ) 
(이게 동작하지 않는 다면 nextval 함수를 이용해야 한다.)
(위에서 강제로 primary key 를 대입했기 때문에 이 구문 동작시키면 에러가 발생할 수 있다. 다시 한 번 동작하면 정상적으로 동작한다. primary key 를 강제로 넣는 동작고 auto increment 하는 것을 섞어 쓰면 안되는 것 같다.)

이 구문을 Upsert 로 만든면

INSERT INTO test_table (id, name , visit) 
	VALUES(default, 'Jane', 1)
ON CONFLICT (id)
DO UPDATE
SET visit = test_table.visit + 1;

라고 적을 수 있으며 당연히 insert 동작만 하게 된다.

Multiple Upsert

프로그램에서 sql 을 이용할 때, 일반적으로 query 갯수를 줄이면 좋다.  여러개의  Upsert 하는 것보다는 한 번에 Upsert 를 하는게 일반적으로 더 빠른 것이다. 
여러개의 Upsert 는 다음과 같이 할 수 있다. 

INSERT INTO test_table (id, name , visit) 
	VALUES(4, 'Mary', 1), (5, 'Anna', 1)
ON CONFLICT (id)
DO UPDATE
SET visit = test_table.visit + 1;

values 다음에 여러 리스트를 적으면 된다.

여기서 데이터의 추가가 필요하다면 존재하지 않는 primary key 대신 default 를 하면 된다.

INSERT INTO test_table (id, name , visit) 
	VALUES(4, 'Mary', 1), (5, 'Anna', 1), (default, 'Henry', 1)
ON CONFLICT (id)
DO UPDATE
SET visit = test_table.visit + 1;

 

excluded 키워드 이용

경우에 따라서 update 시 insert 에서 이용한 값을 재이용하고 싶을 때가 있을 것이다. 이 때는 excluded  키워드를 사용한다.

INSERT INTO test_table (id, name , visit) 
	VALUES(1, 'Mary', 1), (2, 'Anna', 1)
ON CONFLICT (id)
DO UPDATE
SET 
	visit = test_table.visit + 1,
	name = excluded.name

 

sqlalchemy

sqlalchemy 에서 default  라는 Keyword 를 이용할 때는 sqlalchemy.text('default') 해야 한다. 
upsert 는  docs.sqlalchemy.org/en/14/dialects/postgresql.html#updating-using-the-excluded-insert-values
를 참고하면 된다.