[Rustlings] errors6.rs

主要是要練習如何撰寫及應用 error conversion。

Conversion 的部份很單純:

impl ParsePosNonzeroError {
    fn from_creation(err: CreationError) -> ParsePosNonzeroError {
        ParsePosNonzeroError::Creation(err)
    }

    fn from_parsing(err: ParseIntError) -> ParsePosNonzeroError {
        ParsePosNonzeroError::ParseInt(err)
    }
}

比較有趣的是應用的部份。

解法一

用 pattern match。無趣了點,但至少很清楚。

fn parse_pos_nonzero(s: &str)
    -> Result<PositiveNonzeroInteger, ParsePosNonzeroError>
{
    let x = match s.parse() {
        Ok(i) => i,
        Err(e) => return Err(ParsePosNonzeroError::from_parsing(e)),
    };
    PositiveNonzeroInteger::new(x)
        .map_err(ParsePosNonzeroError::from_creation)
}

解法二

利用 ? 簡化解法一。重點是要先做完 map_err() 後才加 ?

fn parse_pos_nonzero(s: &str)
    -> Result<PositiveNonzeroInteger, ParsePosNonzeroError>
{
    let x = s.parse().map_err(ParsePosNonzeroError::from_parsing)?;
    PositiveNonzeroInteger::new(x)
        .map_err(ParsePosNonzeroError::from_creation)
}

解法三

想要用一個 expression 解決?這不就來了嗎?用 and_then() 來串接。

fn parse_pos_nonzero(s: &str)
    -> Result<PositiveNonzeroInteger, ParsePosNonzeroError>
{
    s.parse()
        .map_err(ParsePosNonzeroError::from_parsing)
        .and_then(|x| {
            PositiveNonzeroInteger::new(x)
                .map_err(ParsePosNonzeroError::from_creation)
        })
}

Dev-C++ 的前世今生

Dev-C++ 是一套 Windows 上免費(且開放原始碼)的 C/C++ 整合式開發環境 (IDE)。它的 GUI 部份基本上是使用 Delphi 寫成,而背後的編譯器則是使用 Mingw 版本的 GCC。因為可以免費取得,而且安裝、使用上又方便(跟原始 Mingw GCC 比較的話),所以不少學校在教 C++ 的時候,都建議學生使用這一套 IDE。

但 Dev-C++ 在網路上有幾個常見的分支,常常看到初學者使用非常舊的版本,不但編譯器不支援新的 C++ 語法,而且 Dev-C++ 本身還可能有尚未修正的 bug。在此稍微聊一下 Dev-C++ 常見的幾個版本,並且提出給 C/C++ 初學者的建議。

Continue reading  

[Exercism][Rust] Parallel Letter Frequency

題目網址(需登入 Exercism)

簡單來說,就是要用 concurrency 的方式,將「計算文章中每個字母(數字和標點不計)出現的次數」,分配給 worker_count 個 thread 去做,最後再整合在一起。

輸入的資料,第一個是 &str 的 array slice,第二個是 worker_count

Continue reading  

[Exercism][Rust] Diffie Hellman

題目網址(需登入 Exercism)

這個題目在 Rust 語言方面,沒有什麼太困難的地方。所以重點都是在 modular arithmetic 的運算上。

use rand::Rng;

pub fn private_key(p: u64) -> u64 {
    let mut rng = rand::thread_rng();
    rng.gen_range(2..p)
}

pub fn public_key(p: u64, g: u64, a: u64) -> u64 {
    modular_exp(g, a, p)
}

pub fn secret(p: u64, b_pub: u64, a: u64) -> u64 {
    modular_exp(b_pub, a, p)
}
Continue reading  

Wizardry 開發史 (3) – Wizardry

當 Robert Woodhead 完成 Galactic Attack 時,其實他腦中已經有下一部遊戲的想法了。

他繼續從 PLATO 系統上找靈感。當時 PLATO 系統上有幾個類似 D&DDungeons and Dragons,《龍與地下城》)系統的角色扮演遊戲,像是 dnd(看名字就知道)、Moria(這個名字就是《魔戒》中的「摩瑞亞坑道」)、Oubliette(即「地下城」的法文),以及 Avatar 等。這些遊戲在大學校園中相當受歡迎,而家用電腦的領域中卻沒有類似的遊戲。Robert 思索著該如何將這樣的遊戲系統轉移到 Apple ][ 上。他將這個遊戲計畫的名稱暫定為 Paladin

巧的是,在康乃爾大學中,也有個主修資訊的研究生在做類似的事情。那個傢伙就是 Andrew Greenburg(傳說中的 Werdna)。

Continue reading  

UTF-8 與 BOM

Unicode 簡介

Unicode(中文翻成「萬國碼」或「統一碼」)是一種文字編碼方式。主要的目的,是希望能提供一套統一的標準,以數字的方式表示全世界人類使用的文字,方便利用電腦、網路儲存及傳輸文字資訊。詳細的歷史沿革,網路上可以找到很多文章,在此不再贅述。

關於碼點 Code Point

每一個被 Unicode 規範定義的文字,都有一個獨一無二的數值,我們稱之為「碼點 (Code Point,亦翻作「代碼點」)」。通常我們會用 U+xxxxxxxx 代表 16 進位的數字)這樣的格式來表示碼點。

Continue reading  

[C++11] Ranged For Loop

在以前,若是我們要針對 STL container 中的所有物件做事情,最「方便」的方法是使用 iterator 搭配傳統的 for-loop:

class Entry;
list<Entry> my_list;

void check_entries(void)
{
    for ( list::iterator<Entry> itor = my_list.begin(); itor != my_list.end(); ++itor)
    {
        Entry& e = *itor; // iterator 本身的操作類似 pointer
        // do something...
    }
}

Continue reading  

C++ bool 所占的記憶體空間與資料對齊 (data alignment)

前言

這篇文章的內容,緣起於「程式人雜誌」討論區的這個討論串。這個討論串到最後已經失焦了,不過有幾個重點是值得提出來討論的,於是順手整理出幾個重點。

bool 所占的記憶體空間

C++ 內建的標準的型別中,有一個叫 bool 的型別,用來儲存邏輯運算的 true(真)與 false(假)。由於這種型別的變數就只會有 truefalse 這兩種可能的值,因此,若是我們要儲存這樣的變數的話,理論上來說其實只要 1 個 bit 就夠用了。

然而在實作上,由於不是所有的 CPU 都能夠很有效率地處理單一 bit 的運算,因此通常編譯器會用其他更有效率方式的處理。

C++ 語言標準並沒有硬性規定 bool 要用多大的記憶體空間實作,把這件事留給編譯器自行決定。以 Visual C++ 來說,Visual C++ 4.2 是利用 typedefbool 定義成 int,因此 1 個 bool 變數會占用 4 個 byte 的空間;但在 Visual C++ 5.0 以後的版本,bool 則實作為占用 1 個 byte 的內建型別。在目前 x86 版的 GCC 中,bool 占用的空間也是 1 個 byte;但依據 CPU 架構的不同,可能會有不同的做法。

想要測試系統/編譯器如何實作,可以試著編譯並執行下面這個小程式:

#include <iostream>
using namespace std;
int main(void)
{
    cout << Size of bool =  << sizeof(bool) << endl;
    return 0;
}

資料對齊 (Data Alignment)

首先我們先來看一個小程式:(以下程式均在 x86-64 版的 Ubuntu Linux 上,使用 GCC 編譯執行)

// File: data_align_1.C
#include <iostream>
using namespace std;

struct STRUCT_A {
    bool b;
    int  i;
};

int main(void)
{
    STRUCT_A myVar;
    cout << Size of myVar.b =  << sizeof(myVar.b) << endl;
    cout << Size of myVar.i =  << sizeof(myVar.i) << endl;
    cout << Size of myVar   =  << sizeof(myVar) << endl;
    return 0;
}

編譯與執行結果如下:

cclo@kennethlo-ubuntu:~/projects$ g++ data_align_1.C
cclo@kennethlo-ubuntu:~/projects$ ./a.out
Size of myVar.b = 1
Size of myVar.i = 4
Size of myVar   = 8
cclo@kennethlo-ubuntu:~/projects$

「等一下!明明 STRUCT_A 中的兩個資料成員 bi 的體積分別是 1 和 4 個 bytes,為什麼合起來變成 STRUCT_A 之後,就變成 8 個 bytes 了呢?」 是啊!為什麼呢?我們來把 myVar.bmyVar.i 的記憶體位址印出來看看好了:

// File: data_align_2.C
#include <iostream>
using namespace std;

struct STRUCT_A {
   bool b;
   int  i;
};

int main(void)
{
   STRUCT_A myVar;

   cout << Size of myVar.b =  << sizeof(myVar.b) << endl;
   cout << Size of myVar.i =  << sizeof(myVar.i) << endl;
   cout << Size of myVar   =  << sizeof(myVar) << endl;

   // 印出 myVar 中所有成員的記憶體位址
   cout << endl;
   cout << Addr of myVar.b =  << &myVar.b << endl;
   cout << Addr of myVar.i =  << &myVar.i << endl;
   return 0;
}

執行結果如下:

cclo@kennethlo-ubuntu:~/projects$ ./a.out
Size of myVar.b = 1
Size of myVar.i = 4
Size of myVar   = 8
Addr of myVar.b = 0x7fff41fe3e00
Addr of myVar.i = 0x7fff41fe3e04
cclo@kennethlo-ubuntu:~/projects$

你可以試著多跑幾次,雖然每次跑出來的記憶體位址可能不一樣,但都會符合兩件事實:

  1. myVar.i 的位址一定是 4 的倍數,而且 myVar 的位址(也就是 myVar.b 的位址)也是 4 的倍數。

  2. 雖然 myVar.b 的體積只有 1 byte,但 myVar.i 的位址卻是在「myVar.b 的位址 + 4」,而非「myVar.b 的位址 + 1」。

這其實是作業系統和編譯器幫你做的安排。那為什麼它們要這樣做呢?這和 CPU 的結構設計有關係。

現代大部份的 CPU 在讀取記憶體內容的時候,並不是 1 個 byte 1 個 byte 讀取的,而是依照其資料線的寬度,一次讀取數個 byte(目前一般來說是 4 個 byte,也就是 32 個 bit)的資料。而且通常讀取位址的起始位置也有限制,例如一次要讀 4 個 byte,那麼開始的位址就必須是 4 的倍數。這個單位我們一般稱為 chunk

假設我們有個這樣的組語指令:(下面的組語是參考 GNU Assembler 的語法編出來的,實際 CPU 可能沒有 MOVL 這樣的指令,僅供參考)

# 從絕對位址 0x98760003 開始,搬 4 個 byte 的資料到 R9 暫存器
MOVL         $0x98760003, %r8
MOVL         (%r8), %r9

在大部份 RISC 系列的 CPU(如 ARM、PowerPC 等)上並不支援「跨 chunk」存取記憶體。若是你要求 CPU 執行上面的指令的話,CPU 可能會因為無法執行而產生例外狀況 (exception)。至於 x86 系列的 CPU,雖然可以執行,但一般來說執行效率會比較差。

在不支援「跨 chunk 存取記憶體」的 CPU 上,若是硬要做的話,必須要讀取前後的 2 個 chunk 的內容,然後再湊成我們想要的結果:

# 先將 0x98760000 這個 chunk 搬進來,取出 0x98760003 開始的 1 個 byte。
# 假設這顆 CPU 為 little-endian,則此 byte 為最低的那個 byte。
MOVL         $0x98760000, %r8
MOVL         (%r8), %r10
ANDL         $0x000000FF, %r10  # 留下最後的 1 個 byte

# 然後再讀進 0x98760004 那個 chunk,取出 0x98760004~0x98760006 這 3 個
# byte,往高位元 shift 8 個 bit。
MOVL         $0x98760004, %r8
MOVL         (%r8), %r9
SALL         $0x08, %r9         # R9 往左 shift 8 bits

# 最後再把兩次取得的結果組合成我們想要的內容
ORL          %r9, %r10

原本只需要存取記憶體 1 次,現在必須要存取 2 次,還需要加上額外的運算,可想而知的是速度一定會變慢。

因此,為了克服這個問題,作業系統在配置記憶體的時候會以 chunk 為單位;而編譯器在安排資料結構時,也會避免產生必須跨 chunk 存取的狀況。我們上面的例子,myVar.i 被安排在 0x7fff41fe3e04 而不是 0x7fff41fe3e01,就是這個原因。這樣的行為,我們就稱之為資料對齊 (Data Alignment)

「編譯器一定要這麼雞婆嗎?我就是不想要浪費記憶體空間,就算執行的速度再慢也沒有關係!」 如果你真的有需要的話,沒問題。一般有做資料對齊的編譯器都會提供關掉這個功能的做法。以 Visual C++ 和 GCC 來說,你可以在「不想做資料對齊」的資料結構前後加上兩行 #pragma 指令:

// File: data_align_3.C
#include <iostream>

using namespace std;

#pragma pack(push,1)   /* 加入這一行 */
struct STRUCT_A {
    bool b;
    int  i;
};
#pragma pack(pop)  /* 還有這一行 */

int main(void)
{
    STRUCT_A myVar;
    cout << Size of myVar.b =  << sizeof(myVar.b) << endl;
    cout << Size of myVar.i =  << sizeof(myVar.i) << endl;
    cout << Size of myVar   =  << sizeof(myVar) << endl;

    // 印出 myVar 中所有成員的記憶體位置
    cout << endl;
    cout << Addr of myVar.b =  << &myVar.b << endl;
    cout << Addr of myVar.i =  << &myVar.i << endl;
    return 0;
}

(每一套編譯器的做法可能不太一樣,不過 Visual C++ 和 GCC 在這件事情上剛好有共識。)

輸出的結果如下:

cclo@kennethlo-ubuntu:~/projects$ ./a.out
Size of myVar.b = 1
Size of myVar.i = 4
Size of myVar   = 5
Addr of myVar.b = 0x7fff41fe3e00
Addr of myVar.i = 0x7fff41fe3e01
cclo@kennethlo-ubuntu:~/projects$

嗯,看樣子 myVar.i 的確乖乖地貼在 myVar.b 後面了。很好。

延伸思考

  1. 「強制關閉資料對齊功能」所產生出來的資料結構,和一般情況(開啟資料對齊)所產生出來的資料結構,編譯器會產生出不同的機械語言來處理嗎?我們可以要求編譯器產生組語程式,然後再觀察組語程式的內容;或者可以直接從除錯器的反組譯視窗觀察。

  2. 8 位元的 CPU(例如 8051)、編譯器(例如 Keil C51)會做資料對齊嗎?

參考資料

Swapping Nibbles in Keil C51

今天寫程式的時候,有個地方需要 swap 某個 byte 的兩個 nibbles。其實 8051 原本就有一個 instruction 專門做這件事。若是直接寫組語的話,只要 3 行就搞定了:

    MOV     A,tmp       ; 先將原本的值搬進 accumulator
    SWAP    A           ; swap nibbles
    MOV     tmp,A       ; 先將原本的值搬進 accumulator

問題我是用 C 在寫 code 的,所以我就開始研究是不是有辦法讓 Keil C 幫我產生這樣的 code。

首先,我們用標準的 C 的作法來做:

    tmp = (tmp >> 4) | (tmp << 4);

Keil C 產生出來的組語程式如下:

    MOV     A,tmp
    SWAP    A
    ANL     A,#0F0H
    MOV     R7,A
    MOV     A,tmp
    SWAP    A
    ANL     A,#0FH
    ORL     A,R7
    MOV     tmp,A

這樣翻出來的組語程式雖然看起來很長,不過我們還是可以看出 Keil C 厲害的地方:雖然我們寫的程式碼是「向左/向右 shift 4 個 bits」,但翻出來的並沒有「連續 shift 4 次」這樣的過程(8051 的 shift 一次只能 shift 1 個 bit,所以要 shift 多個 bits 就得用迴圈來跑),而是很聰明地利用 SWAP,分別取得 high/low nibble,再重新組合成 完成的 byte。

這樣的結果讓我蠻興奮的,因為這表示 Keil C 會去看 bit operation 的位數,適時地使用處理 nibble 的指令。也就是說,我有機會找到一種寫法,讓 Keil C 翻出直接 SWAP 的組語程式。

但接下來我就卡住了,因為我找不到更好的寫法去表達 “swap nibble” 這樣的想法。

後來,我在 Keil 的文件中找到一系列 intrinsic routines。很不幸地,裡面還是沒有 swap;但卻_crol_()_cror_() 這樣的 rotate 函式。於是我試著這樣寫:

    tmp = _crol_(tmp,4);

結果翻出來的組語如下:

    MOV     R7,tmp
    MOV     R0,#04H
    MOV     A,R7
    INC     R0
    SJMP    ?C0081
?C0080:
    RL      A
?C0081:
    DJNZ    R0,?C0080
    MOV     tmp,A

結果令人失望:變得更糟了!沒有直接用 SWAP 就算了,還利用迴圈的方式來處理。看樣子這個方法也行不通。

剩下一個方法:寫 inline assembly。但 Keil C 的 inline assembly 實在很難用:只要用了這個功能,該 .C 檔就不能直接編譯成 .OBJ 檔,而必須先產生出組語程式,再另外去組譯這個檔案。很麻煩。站在專案維護的角度來看,代價太高了,所以我放棄這樣的做法。

到頭來,我還是採用一開始的那種做法,畢竟在可維護性、執行速度、code size 等各方面,都算是平衡的做法。只是不能用到 8051 提供的功能,心中還是有些遺憾….

PS: 在 這個討論串中,也有人提到用 intrisic routine 的做法,而且說 Keil C 會幫忙做最佳化;不過我實驗的結果並不是這樣。