hankluo6
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 2020q3 Homework (quiz2) contributed by < `hankluo6` > ### `測驗 1` ```cpp #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <string.h> bool is_ascii(const char str[], size_t size) { if (size == 0) return false; int i = 0; while ((i + 8) <= size) { uint64_t payload; memcpy(&payload, str + i, 8); if (payload & MMM) return false; i += 8; } while (i < size) { if (str[i] & 0x80) return false; i++; } return true; } ``` #### `MMM` - [x] `(d)` `0x8080808080808080` #### 說明 `(i + 8) <= size` 用來檢查現在字元數量 `i` 有沒有超過要檢測的字串長度 `size`,並將要檢測的記憶體位置 `str + i` 後面 8 個 bytes 的值複製至 `payload` 中,下方回傳 `false` 可得知 `payload` 用來檢驗是否為 128 內的 ASCII 碼。 從 `uint64_t` 可知 `payload` 為 8 bytes 位元之變數,每個 bytes 代表一個字元 `char`,而每個字元要檢查其值是否在 128 以內,以 16 進為表示就是為 0x00 ~ 0x79。如果該字元與 0x80 做 AND 運算不為 0 的話,表示該字元之 ASCII 碼為 128 以上。 可將上述字元操作擴充成 8 個字元同時檢測,故答案為 `(d)`。 #### 延伸問題 ##### 1. 解釋上述程式的運作原理,特別是為何用到 memcpy 呢?提示: 與 data alignment 有關,詳閱 [C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) * 使用 `memcpy` 可以讓我們一次操作 8 bytes 的資料,如不使用 `memcpy` 而是直接將字串元素與 `(d)` 做 AND 運算,如 `*(str + i) & 0x8080808080808080`,會被解釋為該字元與 `(d)` 的 AND 運算。 * 使用強制轉型可解決上述問題,將 `payload` 改寫成`uint64 *payload = (uint64_t *) (str + i);` 並做 AND 運算,也許你一樣會得到正確的結果,但其實這種結果並非每次都正確。 * 此種記憶體轉型有可能會造成 [Unaligned data access](https://www.kernel.org/doc/Documentation/unaligned-memory-access.txt),因編譯器不能保證轉型完後,新的 data type 符合它自身的 alignment 需求,在不同的 Arm 處理器下,unaligned data access 造成的行為[不盡相同]((https://heyrick.eu/armwiki/Unaligned_data_access)),但在 intel 處理器下通常能透過多次抓取資料來處理 unaligned data,所以上述轉型才有可能會正確。 * 可以透過編譯指令來提示是否有 unalignmeent data access 的情況發生 1. gcc 8 以上的版本提供編譯選項 `-Wcast-align=strict` 來檢驗是否有 unaligned memory access 的情況發生 ``` gcc -Wcast-align=strict align.c ``` 3. 或是透過交叉編譯器 gcc-arm-linux-gnueabi 將編譯目標設定在 Arm 處理器上: ``` arm-linux-gnueabi-gcc -Wcast-align align.c ``` 上述兩種方法在改寫為 `uint64 *payload = (uint64_t *) (str + i);` 的程式上均會發出下列警告 ``` align.c:14:29: warning: cast increases required alignment of target type [-Wcast-align] uint64_t* payload = (uint64_t *)(str+i); ``` 所以此種轉型方法並不通用。 * 回到 `memcpy` 上,使用 `memcpy` 為甚麼不會有 unaligned data access 的問題?參考 glibc 裡 [memcpy.c](https://github.com/lattera/glibc/blob/master/string/memcpy.c) 的實作, `memcpy` 內部會先將 unalignment 的部分使用 byte copy 來對齊,才使用 word copy 來複製接下來的資料,而當剩餘資料長度小於 word 時,再使用 byte copy 複製剩餘資料。 ##### 2. 將上述技巧應用於「給予一個已知長度的字串,檢測裡頭是否包含有效的英文大小寫字母」 詳細可參考測驗 5,概念為當字元在 `A-Z` 或 `a-z` 時,將第該字元的第 8 個 bit 設為 1,用該 bit 判斷是否為英文大小寫字母。 ```c bool is_alpha(char *s, size_t n) { size_t k = n / 8; for (size_t i = 0; i < k; i++, s += 8) { uint64_t *chunk = (uint64_t *) s; uint64_t A = *chunk + PACKED_BYTE(128 - 'A'); uint64_t Z = *chunk + PACKED_BYTE(127 - 'Z'); uint64_t a = *chunk + PACKED_BYTE(128 - 'a'); uint64_t z = *chunk + PACKED_BYTE(127 - 'z'); if (!(((A ^ Z) & PACKED_BYTE(0x80) | (a ^ z) & PACKED_BYTE(0x80)) & PACKED_BYTE(0x80))) return false; } k = n % 8; while (k--) { char c = *(s++); if (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))) return false; } return true; } ``` ##### 3. 承 (2),考慮常見的英文標點符號,判斷輸入字串是否為有效的字元集,避免逐一字元比對 先來定義何謂 **"常見的英文標點符號"**,這邊我使用 [7esl.com](https://7esl.com/punctuation-marks/) 提供的 14 個常用的標點符號。 接著把標點符號與對應的 ASCII Code 列出來觀察 | Marks | Decimal | Hexadecimal | | ------- | ------- | ------------------- | | (space) | 32 | 0010 0000 | | ! | 33 | 0010 0001 | | " | 34 | 0010 0010 | | ' | 39 | 0010 0111 | | () | 40,41 | 0010 1000,0010 1001 | | , | 44 | 0010 1100 | | - | 45 | 0010 1101 | | . | 46 | 0010 1110 | | / | 47 | 0010 1111 | | : | 58 | 0011 1010 | | ; | 59 | 0011 1011 | | ? | 63 | 0011 1111 | | \ | 92 | 0101 1010 | | [] | 91,93 | 0101 1011,0101 1101 | 為了計算方便,我將 `\`也加入至符號中。 :::info …(Ellipsis) 不在 Ascii Code 裡,故不考慮 ::: 接著來看一個 SSE 常用到的 type:`__m128i`,其定義為 ```c typedef union __declspec(intrin_type) __declspec(align(16)) __m128i { __int8 m128i_i8[16]; __int16 m128i_i16[8]; __int32 m128i_i32[4]; __int64 m128i_i64[2]; unsigned __int8 m128i_u8[16]; unsigned __int16 m128i_u16[8]; unsigned __int32 m128i_u32[4]; unsigned __int64 m128i_u64[2]; } __m128i; ``` 可以看到為一個裝有 128 bits 的 union,因為是 union,所以內部記憶體空間是共享的,總共佔有 128 bits。 接著來看使用到的指令,以下指令皆可從 [Intel Intrinsics Guide](https://software.intel.com/sites/landingpage/IntrinsicsGuide/) 中找到。 1. `_mm_setr_epi8` 將參數裡的字元以相反的順序放入到目標變數(`__m128i`)中 e.g. `_mm_setr_epi8(0x10, 0x11, 0x12, ..., 0x24, 0x25);` 會回傳一個 type 為 `__m128i` 且內容為 `0x2524232221...121110` 的數字 2. `_mm_cmpestrm` 會依照所選擇的 mode 來決定如何比對兩個不同的字串,如果符合就在該字元的位置存放 `0xFF`,而 mode 選擇以下: * `_SIDD_UBYTE_OPS` 表示字元為 8 bits * `_SIDD_CMP_RANGES` 表示比較 [range](https://doc.rust-lang.org/core/arch/x86_64/constant._SIDD_CMP_RANGES.html),檢測字元是否在給定的範圍內 * `_SIDD_UNIT_MASK` 回傳 bytes/word 的 mask 3. `_mm_test_all_ones` 如果數字內每個 bit 都為 1 則回傳 1,否則回傳 0 主程式的邏輯很簡單,首先使用 `_mm_setr_epi8` 將合理字元的範圍 pack 成 16 bytes 的變數。接著,每次對 16 bytes 的字串進行 `_mm_cmpestrm`,如果符合定義的範圍(`valid_bits`),則該函式會回傳 16 bytes 的 1,並透過 `_mm_test_all_ones` 來檢查。 如果剩餘字串少於 16 bytes,則需要另外判斷。我透過 `check` 函式將 16 bytes 的值拆分成兩個 8 bytes,並進行 bitwise 的檢查,當出現不為 1 時表示出現不在範圍內的字元。 :::warning 這邊原本是想直接用函式來檢查未滿 16 bytes 的值是否全為 1,但翻閱 Guide 找不到相關的函式,也不能直接對 `__m128i` 進行 bit 操作,故只能寫成冗長的程式碼,如果有其他想法歡迎提供。 ::: ```c bool check(__m128i var, size_t last_size) { uint64_t val[2]; memcpy(val, &var, sizeof(val)); last_size *= 8; //bytes to bits if (last_size > 64) { if (val[0] != 0xFFFFFFFFFFFFFFFF) return false; last_size -= 64; while (last_size) { if (!((var[1] >> (last_size - 1)) & 1)) return false; --last_size; } } else { while (last_size) { if (!((var[0] >> (last_size - 1)) & 1)) return false; --last_size; } } return true; } bool is_valid(const char str[], size_t size) { __m128i valid_bits = _mm_setr_epi8('a', 'z', 'A', ']', '?', '?', ',', '/', ' ', '"', ':', ';', '\'', ')', 0, 0); const char mode = _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES | _SIDD_UNIT_MASK; unsigned int i = 0; while (i != size) { size_t last_size = (i + 16) <= size ? 16 : (size - i); __uint128_t payload; memcpy(&payload, str + i, last_size); __m128i *ptr = (__m128i*)&payload; const __m128i mask = _mm_cmpestrm(valid_bits, 14, *ptr, last_size, mode); if (last_size != 16 && !check(mask, last_size)) { return false; } else if (!_mm_test_all_ones(mask)) return false; i += last_size; } return true; } ``` :::info 踩坑紀錄: 使用 `_mm_cmpestrm` 時輸入將 `const char mode = _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES | _SIDD_UNIT_MASK;` 放入第五個參數,卻一直跳出 `the fifth argument must be an 8-bit immediate`。因為 gcc 預設不會將在編譯時期決定 function 內的 const 的值,所以才一直報錯。 解決方法為使用任一編譯器優化選項 `-O1~O3` 來讓編譯器在編譯時期就決定 `mode` 的值。 參考 stack overflow: 1. [_mm256_slli_si256: error “last argument must be an 8-bit intermediate](https://stackoverflow.com/questions/31315491/mm256-slli-si256-error-last-argument-must-be-an-8-bit-intermediate) 2. [Is variable defined by const int determined at compilation time?](https://stackoverflow.com/questions/27048025/is-variable-defined-by-const-int-determined-at-compilation-time) ::: ### `測驗 2` ```c uint8_t hexchar2val(uint8_t in) { const uint8_t letter = in & MASK; const uint8_t shift = (letter >> AAA) | (letter >> BBB); return (in + shift) & 0xf; } ``` #### `MASK` - [x] `(c)` `0x40` #### `AAA` - [x] `(a)` `3` #### `BBB` - [x] `(b)` `6` #### 說明 先將各字元的編碼寫下來 | char | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | hex | | ----- | --- |:---:| --- | --- | --- | --- | --- |:---:| --- | | `'0'` | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0x30 | | `'9'` | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 0x39 | | `'a'` | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0x61 | | `'f'` | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 0 | 0x66 | | `'A'` | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0x41 | | `'F'` | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 |0x46 可以看到數字 `0` ~ `9` 與英文字 `a` ~ `f` 或 `A` ~ `F` 的差別在第 6 個 bit 是否為 1,而 `a` ~ `f` 與 `A` ~ `F` 的差別則是在第 5 個 bit 是否為 1。 從變數名稱 `letter` 可以猜測應為判斷是否為字母,判斷字母的 bit 為第 6 個 bit,所以 `MASK` 應為 0x40;再來因為 `return` 的值會與 0xf 做 AND,可以只觀察後 4 個 bits,數字的後 4 個 bits 剛好就是對應到的該數字的值,字母的後 4 個 bits 不管是大寫還是小寫皆相同,所以可以一起做判斷。 字母的後 4 個 bits 為該字母所代表的數字之個位數字,亦即:`'a'` 表示 `10`,則後 4 個 bits 為 `1`,`'d'` 表示 `13`,則後 4 個 bits 為 3。所以我們只需將後 4 個 bits 加上數字 10 就行了,用第 6 個 bit 的位移來組合出數字 10,故答案為 `(a)` 與 `(b)`。 #### 延伸問題 ##### 1. 將 hexchar2val 擴充為允許 "0xFF", "0xCAFEBABE", "0x8BADF00D" 等字串輸入,且輸出對應的十進位數值 Hexspeak ```c /* input must start with 0x and the string length need to less than 32 */ uint64_t hex2val(char *s) { s += 2; uint64_t val = 0; while (*s != '\0') { if (*s == 'x' || *s == 'X') break; val <<= 4; val |= hexchar2val((uint8_t)(*(s++))); } return val; } ``` 程式邏輯很簡單,開頭 `s += 2` 用來移除 hex 中 `0x` 的部分,之後從字串頭開始將每個字元轉換為對應的數值。向左位移 4 bits 是因為每個字元在 16 進制表示中只佔用 4 個 bits。所以每次 `*s` 往低位元移動時,轉換的值皆要往高位元移動 4 bits 來維持數字的正確性,而 OR 運算則是將新轉換的字元之值加入到現有的 `val` 當中。 ### `測驗 3` ```c const uint32_t D = 3; #define M ((uint64_t)(UINT64_C(XXX) / (D) + 1)) /* compute (n mod d) given precomputed M */ uint32_t quickmod(uint32_t n) { uint64_t quotient = ((__uint128_t) M * n) >> 64; return n - quotient * D; } ``` #### `XXX` - [x] `(f)` `0xFFFFFFFFFFFFFFFF` #### 說明 取餘數的公式可以寫成 $n\ mod\ d = n - \lfloor{\frac{n}{d}}\rfloor \times d$,而 $\lfloor\frac{n}{d}\rfloor$ 根據測驗 3 的說明可以改寫為 $\lfloor n \times \frac{\frac{2^N}{d}}{2^N}\rfloor$。對照程式碼,右移 64 bytes 表示除以 $2^{64}$,所以 $N$ 應為 64。 接著我們嘗試將 $\lfloor{\frac{n}{d}}\rfloor$ 改寫為 $\lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor$,可以從後面推導回來: $$ \begin{equation} \begin{split} \lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor &= \lfloor \frac{n}{2^N} \times \frac{2^N + r}{d}\rfloor \\ &= \lfloor \frac{n \times 2^N}{d \times 2^N} + \frac{n \times r}{d \times 2^N}\rfloor \\ &= \lfloor \frac{n}{d} + \frac{n \times r}{d \times 2^N}\rfloor \\ &= \lfloor q + \frac{k}{d} + \frac{n \times r}{d \times 2^N}\rfloor \\ &= q + \lfloor \frac{k}{d} + \frac{n \times r}{d \times 2^N} \rfloor\\ &= \lfloor\frac{n}{d}\rfloor \quad \text{if} \quad \frac{k}{d} + \frac{n \times r}{d \times 2^N} < 1 \end{split} \end{equation} $$ 其中: * $r = -2^N\ mod\ d$,用來將 $\frac{2^N}{d}$ 無條件進位到整數。 * $k = n\ mod\ d$,用來將 $\frac{n}{d}$ 無條件捨去到整數,並用 $q$ 表示。 * $q$ 為一整數,所以可以從 floor function 中提出。 再來就能進行完整的推導: $$ \begin{equation} \begin{split} \lfloor\frac{n}{d}\rfloor &= \lfloor n \times \frac{\frac{2^N}{d}}{2^N}\rfloor \\ &= \lfloor \frac{n}{2^N} \times \lceil\frac{2^N}{d}\rceil\rfloor \quad \text{if} \quad \frac{k}{d} + \frac{n \times r}{d \times 2^N} < 1 \\ &= \lfloor \frac{n}{2^N} \times (\lfloor\frac{2^N - 1}{d}\rfloor+1)\rfloor \\ \end{split} \end{equation} $$ 上述 ceil function 的轉換可參考 [wikipedia](https://en.wikipedia.org/wiki/Floor_and_ceiling_functions#Quotients) 的 Quotients 的部分。 與程式碼比對,$M$ 為 $\lfloor\frac{2^N - 1}{d}\rfloor + 1$,所以 `XXX` 為 $2^N - 1$,答案為 `(f)`,特別注意 `quotient` type 為 `uint64_t`,與 `UINT64_C`,其目的皆是在實現公式中 floor function 的部分。 我們對再來深入探討條件成立的情形,因 $n < 2^{32},N = 64$,所以: $$ \begin{equation} \begin{split} \frac{k}{d}+\frac{n \times r}{d \times 2^N} &< \frac{k}{d} + \frac{r}{d} \times \frac{2^{32}}{2^{64}} \\ &=\frac{k}{d} + \frac{r}{d} \times \frac{1}{2^{32}} \\ &<\frac{d-1}{d} + \frac{d-1}{d} \times \frac{1}{2^{32}} \\ &= 1 \end{split} \end{equation} $$ 上述式子要成立,在 $N = 64$ 且 $n = 2^{32}$ 的情況下,$d$ 必須小於 $2^{32} + 1$。 再來探討 $n$ 的範圍,從上面敘述已經得知 $d < 2^{32} + 1$,所以將情況對調,當 $d = 2^{32} + 1$ 時,$n$ 就必須小於 $2^{32}$,隨著 $d$ 減少,$n$ 能容許的範圍就越大。當 $d = 3$ (此例) 時,$n$ 在 $2^{63}$ 以下都能滿足條件。 從以上兩點可知此程式在 32 bits 的範圍下能夠正常運作,我們能將 $d$ 寫成更通用的形式,$d < \frac{2^N}{n} + 1$。當我們想要在更高位元的操作,只需要調整 $N$ 與 $n$ 來滿足需要的位元數。 #### 延伸問題 ##### 1. 由 Facebook 公司所維護的 [jemalloc](https://github.com/jemalloc/jemalloc) 是個高效能的記憶體配置器 (memory allocator,即 malloc 和 free 函式對應的實作),特別在多核多執行緒的環境有優異表現,在其原始程式碼 [include/jemalloc/internal/div.h](https://github.com/jemalloc/jemalloc/blob/dev/include/jemalloc/internal/div.h) 和 [src/div.c](https://github.com/jemalloc/jemalloc/blob/dev/src/div.c) 也運用類似的技巧,請閱讀原始程式碼,並抽離快速除法實作為獨立的程式碼,說明運作原理,也該撰寫測試程式,比較透過處理器的整數除法指令及本技巧的效能差距; 將 `jemalloc` 的除法操作抽取出來 ```c typedef struct div_info_s div_info_t; struct div_info_s { uint32_t magic; }; void div_init(div_info_t *div_info, size_t divisor); static inline size_t div_compute(div_info_t *div_info, size_t n) { assert(n <= (uint32_t) - 1); size_t i = ((uint64_t)n * (uint64_t)div_info->magic) >> 32; return i; } void div_init(div_info_t *div_info, size_t d) { assert(d != 0); assert(d != 1); uint64_t two_to_k = ((uint64_t)1 << 32); uint32_t magic = (uint32_t)(two_to_k / d); if (two_to_k % d != 0) { magic++; } div_info->magic = magic; } ``` 其內容與本測驗中的實作非常相似,只有兩個地方不同: 1. `jemalloc` 使用的 $M$ (或是稱為 **Magic Number**) 為 $2^{32}$,而此測驗中用的為 $2^{64}$。 2. 在上面推導中出現的 $\lceil\frac{2^N}{d}\rceil$,因 c 語言沒有有效率的 ceil function,在 `jemalloc` 中使用餘數判斷是否為整除,而此測驗中則是轉換為 floor function 來實作。 :::danger `jemalloc` 使用 $2^{32}$ 精度太低,註解只有寫道 We bound n < 2^32, and don't support dividing by one.,好像沒有關於 divisor 的限制。根據我在上面的推導,使用 $2^{32}$ 只能保證在 16 bits 下答案正確 ::: 這裡給出其中一個例子:$\lfloor\frac{9510000}{16655}\rfloor=570$,但 `jemalloc` 會給出 $571$。 接著量測兩種除法的效率,分別是考慮計算 **Magic Number** 的時間與不考慮的時間。 分別測試 3 種除法之效率: 1. 內建除法運算子 2. 此測驗題中使用 floor function (轉型) 的除法,或稱為 [lkk ](https://arxiv.org/pdf/1902.01961.pdf) 3. `jemalloc` 使用的 `div` 為了讓結果更具有代表性,將本測驗題中的 $N$ 改為 $32$。我將從 $1\sim2^{20}$ 中以 $1000$ 的間隔抽取一次 $n$,並對該數字測試 1000 次來盡量縮小標準差。注意 $d$ 的選擇必須符合公式 $d < \frac{2^N}{n} + 1$。並且為了減少 function call 造成的時間差異 (雖然有 inline,但編譯器不一定會使用),我將 `jemalloc` 的程式碼展開成 one line 的形式。並將 Magic Number 的精度改為 $2^{64}$ 首先是不考慮 Magic Number 的除法效率,可以發現 `jemalloc` 與 `lkk` 在計算除法的部分是一樣的,所以只考慮 `jemalloc` 與除法運算子的效率就好。 測試程式: ```c #include <stdint.h> #include <stdbool.h> #include <stdio.h> #include <assert.h> #include <time.h> #include <stdlib.h> #define MIN 3 #define MAX 0x80000000 int main() { srand(time(NULL)); div_info_t div; struct timespec tt1, tt2; for(uint32_t i = MIN; i < MAX; i += 100000) { size_t d = 0; size_t ans[100001][2]; size_t times = 0; time_t time1, time2; while ((d = rand() % i) < 2) ; jemalloc_init(&div, d); clock_gettime(CLOCK_MONOTONIC, &tt1); while (times++ <= 100000) ans[times][0] = (i * (__uint128_t)div.magic) >> 64; clock_gettime(CLOCK_MONOTONIC, &tt2); time1 = (tt2.tv_nsec - tt1.tv_nsec); times = 0; clock_gettime(CLOCK_MONOTONIC, &tt1); while (times++ <= 100000) ans[times][1] = i / d; clock_gettime(CLOCK_MONOTONIC, &tt2); time2 = (tt2.tv_nsec - tt1.tv_nsec); assert(ans[100000][0] == ans[100000][1]); printf("test %d / %ld, time on jemalloc = %ldns, time on basic divide = %ldns\n", i, d, time1, time2); } } ``` 我將 divisor 設為 i 以內,原因是我怕 divisor 大於 dividend 時編譯器會利用比大小來回傳(實際上會不會尚未研究)。每個測資皆跑 100000 次來減少標準差。另外我也將 `jemalloc` 的精度調整為 $2^{64}$。 ![](https://i.imgur.com/dJsmhna.png) 可以看到 `-O0` 的效能差距相當大,~~而當編譯器加速後,兩者除法之間也有些許的差異。不管怎樣,在當 divisor 不用重新計算時,`jemalloc` 的效率皆比內建除法還好。~~ :::info 原先有測試 `O1~O3` 的效能,但發現編譯器加速的部分過於複雜,擔心測試不夠完整,故先移除。 ::: 再來是考慮當 divisor 要重新計算時,計算 **Magic Number** 的除法效率 餘數判斷是非常耗時的操作,在 `jemalloc` 中的 `two_to_k % d != 0` 這個判斷式應該可以再簡化成判斷 `d` 是否為 2 的倍數,改為 `d & (d - 1) == 0` 來看看時間是否比較快。 所以這部分有 4 種待測除法 1. 內建除法運算子 2. `lkk` 使用的 `div` 3. `jemalloc` 使用的 `div` 4. 更改 `jemalloc` 後的 `div` ![](https://i.imgur.com/9OBQD8C.png) 可以看到如果每次 divisor 都不同,計算 Magic Number 的成本比直接使用一般除法還高,~~而當編譯器加速得越多,不同方法之間的速度差也會越來越少。~~ 這邊也順便證明了我去掉模運算的版本速度比原先 `jemalloc` 還要快上許多,但還是比不上 `lkk` 中使用轉型實現 floor function 的方法。 所以只要在固定的 divisor 上跑多次除法,先決定好 Magic Number 的值後,之後的除法效率就能夠超越內建的除法運算,但當每次 divisor 都會變動時,把計算 Magic Number 的成本加進來後,效率就可能比內建的除法運算差! ### `測驗 4` ```c bool divisible(uint32_t n) { return n * M <= YYY; } ``` #### `YYY` - [x] `(c)` `M - 1` #### 說明 證明參考 [sammer1107](https://hackmd.io/@sammer1107/ryHs1sESP#%E6%B8%AC%E9%A9%97-4) 同學的推導 * 當 $n \ {\mid} \ d$ 時,$n \times M <= M - 1$ * 當 $n \ {\nmid} \ d$ 時,$n \times M > M - 1$ 分成兩個 case,$n \ {\mid} \ d$ 與 $n \ {\nmid} \ d$,算式內數值需對 $2^{64}$ 取餘數,因 type 為 `uint64_t`。 1. $n \ {\mid} \ d$,$n = 3q$: $$ \begin{align} n \times M &= 3q \times \lceil\frac{2^{64}}{3}\rceil \\ &= 3q \times \frac{2^{64} + 2}{3} \\ &= q2^{64} + 2q \quad (\text{mod} \ \ 2^{64})\\ &= 2q \end{align} $$ 再來思考一下 $q$ 的上界: $$ \begin{align} n = 3q < 2^{32} \\ q < \frac{2^{32}}{3} \end{align} $$ 帶回原式完成證明: $$ \begin{align} n \times M &= 2q \\ &< \frac{2^{33}}{3} \\ &< M - 1 \\ &= \frac{2^{64}+2}{3} - 1\\ &= \frac{2^{64} - 1}{3} \end{align} $$ 2. $n \ {\mid} \ d$,$n = 3q + r,0<r<3$ 我們將 $r$ 取 1,因 $3q + 1 > M - 1 \implies 3q + 2 > M - 1$: $$ \begin{align} n \times M &= (3q+r) \times \lceil\frac{2^{64}}{3}\rceil \\ &= (3q + 1) \times \frac{2^{64} + 2}{3} \\ &= q2^{64} + 2q + \frac{2^{64} + 2}{3} \quad (\text{mod} \ \ 2^{64})\\ &= 2q + \frac{2^{64} + 2}{3} \\ &\geq \frac{2^{64}+2}{3} \\ &> M - 1 \\ &= \frac{2^{64}-1}{3} \end{align} $$ $n \times M$ 是有可能等於 $M$ 的,如果 rhs 為 $M$ 而不是 $M - 1$ 的話,演算法有可能會有誤。如 $n = 1$ 時等號相等,`divisible` 會 return 1 而不是 0。 ### `測驗 5` ```c #include <ctype.h> #include <stddef.h> #include <stdint.h> #define PACKED_BYTE(b) (((uint64_t)(b) & (0xff)) * 0x0101010101010101u) /* vectorized implementation for in-place strlower */ void strlower_vector(char *s, size_t n) { size_t k = n / 8; for (size_t i = 0; i < k; i++, s += 8) { uint64_t *chunk = (uint64_t *) s; if ((*chunk & PACKED_BYTE(VV1)) == 0) { /* is ASCII? */ uint64_t A = *chunk + PACKED_BYTE(128 - 'A' + VV2); uint64_t Z = *chunk + PACKED_BYTE(128 - 'Z' + VV3); uint64_t mask = ((A ^ Z) & PACKED_BYTE(VV4)) >> 2; *chunk ^= mask; } else strlower(s, 8); } k = n % 8; if (k) strlower(s, k); } ``` #### `VV1` - [x] `(e)` `0x80` #### `VV2` - [x] `(b)` `0` #### `VV3` - [x] `(a)` `(-1)` #### `VV4` - [x] `(e)` `0x80` #### 說明 先了解 `PACKED_BYTE` 的作用,從前面的 `& 0xff` 可知前面這部分只有前 1 個 byte 有值,再來後面的乘法可看成 8 個 bytes,每個 bytes 皆為 1,所以 `PACKED_BYTE` 可以知道為將 1 個 byte 的資料擴充成 8 個 bytes。 接著來看 `VV1`,從第一個 `if` 後方的註解 `is ASCII?` 可知該判斷式用來判斷是否為 ASCII code,下方 `else` 裡使用 `strlower`,應是用來處理 extend ASCII 的部分,所以要判斷是否為 ASCII code 的話 `VV1` 需選擇 0x80 (原因可參考測驗1)。 來看 `mask` 的用途,從 `*chunk ^= mask` 可以知道 `mask` 要將 `*chunk` 內 `A-Z` 的 ASCII code 轉為 `a-z`。根據[...](),對每個 `A-Z` 字元的第 6 個 bit 做 XOR 運算就能轉成 `a-z`。故 `mask` 為在 `*chunk` 內每個為 `A-Z` 字元的部分的第 6 個 bit 為 1,其餘為 0。 由上述可知,`((A ^ Z) & PACKED_BYTE(VV4)) >> 2` 應該會在 `A-Z` 的字元中第 6 個 bit 產生 1,所以 `PACKED_BYTE(VV4)` 在第 8 個 bit 上產生 1,故 `VV4` 為 0x80。而 `A ^ Z` 就是用來檢查是否為 `A-Z` 間的字元,並將結果設置在第 8 個 bit 上。 要讓 XOR 為 True,必須一邊為 True,一邊為 False,但仔細觀察 `A` 與 `Z` 的程式碼,會發現在第 8 個 bit 上,`Z` 為 True 時,`A` 必定為 True。因 `Z` 比 `A` 還要多減了 `'Z' - 'A'` 的 ASCII code。 所以我們目的是讓 `A` 的第 8 個 bit 為 1,`Z` 的第 8 個 bit 為 0。則要讓 `128 - 'A' + VV2 + c` 內當 `c` 的 ASCII code 為 `A` 以上時,第 8 個 bit 為 1,`VV2` 需為 0,而在 `c` 的 ASCII code 大於 `Z` 時,`128 - 'Z' + VV3 + c` 的第 8 個 bit 要為 0,故 `VV3` 為 -1。 #### 延伸問題 ##### 1. 將前述測試程式 main 函式的 char str[] 更換為 char *str,會發生什麼事?請參閱 C 語言規格書,說明 string literal 的行為 改成 `char *str` 會造成 Segmentation fault,試著用 pointer to char 指向已經建立 String Literals 上,`char *p_str = str`。程式卻能正常執行。 所以推測問題不是 pointer 的問題,而是建立變數時期的問題,從規格書裡找答案。 在規格書 [6.4.5 String literals](http://c0x.coding-guidelines.com/6.4.5.html) 中提到 > a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals. The multibyte character sequence is then used to initialize an array of static storage duration and length just sufficient to contain the sequence. > > If the program attempts to modify such an array, the behavior is undefined. 在 [6.7.8 Initialization](http://c0x.coding-guidelines.com/6.7.8.html) 中也有寫道 >**`char *p = "abc";`** defines p with type “pointer to char” and initializes it to point to an object with type “array of char” with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined. 所以 `p` 為 "pointer to char" 的 type,指向一個 "array of char" 這個 object。在 [Frequently Asked Questions](http://c-faq.com/decl/strlitinit.html) 中也有提到,除了初始化時的變數 type 為 array of char 以外,string literal 會建立一個 unnamed, static array of characters 然後存放在 read-only 的記憶體位置,再將該位置回傳給 pointer to char 這個變數。 統整一下: 1. `char p[] = "hello";` 會被轉譯成 `char p[] = {'h', 'e', 'l', 'l', 'o', '\0'}`,所以操作該變數就跟一般的 array 操作一樣。 2. `char *p = "hello";` 此行為會在 read-only 的記憶體上建立 `hello` 這個字串,並將 `p` 指向該記憶體位置。 一個在 [stack overflow](https://stackoverflow.com/questions/1704407/what-is-the-difference-between-char-s-and-char-s) 上的例子我覺得能幫助理解: ```c char *s0 = "hello world"; char s1[] = "hello world"; 0x01 0x02 0x03 0x04 0x00008000: 'h' 'e' 'l' 'l' 0x00008004: 'o' ' ' 'w' 'o' 0x00008008: 'r' 'l' 'd' 0x00 ... s0: 0x00010000: 0x00 0x00 0x80 0x00 s1: 0x00010004: 'h' 'e' 'l' 'l' 0x00010008: 'o' ' ' 'w' 'o' 0x0001000C: 'r' 'l' 'd' 0x00 ``` 當使用 `char *s0 = "hello world";`時, 編譯器實際上做了以下事情 ```c static char __unnamed[] = "hello world"; char *c = __unnamed; ``` 至於這個存放字串常量的 read-only 空間在哪裡呢?是放在一個名為 rodata 的地方,這邊 ro 指的就是 read-only,在程式內宣告的 `const` 變數,`#define` 的常量皆放置在此空間內,此位置與 [Data segment](https://en.wikipedia.org/wiki/Data_segment) 為兩種不同的空間。 可以寫個小程式來測試: ```c #include <stdio.h> char *s = "hello"; int main() { char *str1 = "hello"; char *str2 = "world"; char str3[] = "!!"; return 0; } ``` 輸出 assembly code 來看編譯器是如何處理這些字串的 ``` gcc -S str.c ``` 可以看到以下程式碼: ```c 3 .globl s 4 .section .rodata ... 11 s: 12 .quad .LC0 13 .section .rodata ``` 可以看到編譯器將字串常量放入 .rodata 裡面,且在 main 裡面的 `str1` 也會指到字串 `"hello"` 所在的地方,意思是 `s == str1`,而使用陣列方式宣告的 `char str3[]` 則是將字串內的值直接存入變數中 ```c 35 movw $8481, -11(%rbp) ``` 可以印出四個變數所指向的位置來檢查: ```c s = 0x556e92e397a4 str1 = 0x556e92e397a4 str2 = 0x556e92e397aa str3 = 0x7ffd2a9f0366 ``` 與預測的結果符合,`s` 與 `str1` 都指向在 rodata 中的 `"hello"` 字串位置。也可以注意到 `s, str1, str2` 三個位置都為 0x55 開頭,表示放在同樣的區塊,也就是 rodata 區塊,而 `str3` 與其他三著不同,是放置在 stack 區塊。 也可以透過 objdump 來查看 rodata 內的內容 ``` objdump -s -j .rodata a.out ------------------------------------- pointer: file format elf64-x86-64 Contents of section .rodata: 0730 01000200 68656c6c 6f00776f 726c6400 ....hello.world. ``` 清楚的看到 `"hello"` 與 `"world"` 都被放到 rodata 中。 ### `測驗 6` ```c int singleNumber(int *nums, int numsSize) { int lower = 0, higher = 0; for (int i = 0; i < numsSize; i++) { lower ^= nums[i]; lower &= KKK; higher ^= nums[i]; higher &= JJJ; } return lower; } ``` #### `KKK` - [x] `(c)` `~higher` #### `JJJ` - [x] `(d)` `~lower ` #### 說明 要用 XOR 來比對出現一次與出現三次的差別,只用一個變數是不夠的,因一次 XOR 與三次 XOR 會造成相同結果。所以我們使用兩個變數 `lower` 與 `higher` 用來記錄出現一次與出現兩次。 我們先只看一個數 `nums[i]`,把 `lower` 與 `higher` 看成兩個 set,用來記錄使用過的 bits。 `lower` 有兩種情形,分別是 `lower` 存有 `nums[i]` 的 bits 與不存有 `nums[i]` 的 bits。經過 `lower ^= nums[i]` 後,`lower` 進到下個狀態,存有 `nums[i]` 的 bits 會被移除,沒有存 `nums[i]` 的話,則會將 `nums[i]` 加入 `lower` 中。 `higher` 同理,有兩種情形,存有 `nums[i]` 的 bits 與不存有 `nums[i]` 的 bits。故可以推測當 `nums[i]` 出現一次時,存到 `lower` 中,出現第二次時,從 `lower` 中移除,並加入至 `higher` 中,第三次出現則從 `higher` 中移除。 這樣答案就很清楚了,可以對照每行程式碼的行為: 1. `lower ^= nums[i]` 將 `nums[i]` 從 `lower` 中加入或移除 2. `lower &= ~higher` 如果 `nums[i]` 已經在 `higher` 當中(第二次出現),則 `lower` 不需要再加入 `nums[i]`,將其移除 3. `higher ^= nums[i]` 將 `nums[i]` 從 `higher` 中加入或移除 4. `higher &= ~lower` 如果 `nums[i]` 已經在 `lower` 當中(第一次出現),則 `higher` 不需要再加入 `nums[i]`,將其移除 畫成 state diagram 可以更好理解,可以想像當 `nums[i]` 不同時,出現 3 次的值還是會從 set 中移除,而出現 1 次的值永遠會存在 `lower` 當中,所以最後回傳的 `lower` 就是答案。 ```graphviz digraph G { rankdir=LR; node node1 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {} higher = {}"] n1; node node2 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {nums[i]} higher = {}"] n2; node node3 = [fixedsize=true, width = 2, height = 0.8, label = "lower = {} higher = {nums[i]}"] n3; n1->n2 [label = "first times"]; n2->n3 [label = "second times"]; n3->n1 [label = "third times"]; } ``` #### 延伸問題 ##### 1. 請撰寫不同上述的程式碼,並確保在 LeetCode [137. Single Number II](https://leetcode.com/problems/single-number-ii/) 執行結果正確且不超時; 我使用 64 bits 來儲存 32 bits 內每個 bit 出現的情況。 首先,我將每個數字擴充成 64 bits,並且中間間隔 1 個 0 用來存放出現 2 次以上的 bit。 類似下方表格這種轉換: | number | 4 bits | 8 bits | | -------- | -------- | -------- | | 5 | <font color="blue">0101</font> | <font color="#f00">0</font><font color="blue">0</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">0</font><font color="#f00">0</font><font color="blue">1</font> | | 14 | <font color="blue">1110</font> | <font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">0</font> | | 16 | <font color="blue">1111</font> | <font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font><font color="#f00">0</font><font color="blue">1</font> | 這樣每個數字原本 32 bits 都擁有兩個 bit 可以儲存每個 bit 出現的次數,接著就是當出現 3 次,也就是該 bit 在擴充的位元裡出現 11 的時候,要將這兩個 bit 清空。 我先對現在累加的數字 `bit_set` 進行 `mask`,`mask` 的值為 `0x101010...1010`,用來檢查該 bit 是否累加超過 2 次以上。如果該結果為 1,再來右移 1 個 bit 檢查前一個 bit 是否也為 1,假如這兩個 bit 都為 1,就從 bit_set 內部清空。 最後再把結果的 64 bits 還原成 32 bits 並回傳。 ```c int singleNumber(int *nums, int numsSize) { uint64_t mask = 0xAAAAAAAAAAAAAAAA; uint64_t bit_set = 0; int ans = 0; for (int i = 0; i < numsSize; ++i) { uint64_t num_bits = 0; for (int j = 0; j < 32; ++j) { uint64_t offset = nums[i] & ((uint64_t)1 << j); num_bits |= (offset << j); } bit_set += num_bits; uint64_t t = (bit_set & mask) >> 1; t = ((bit_set & t) << 1) | t; bit_set &= ~t; } for (int i = 0; i < 32; ++i) { ans |= (bit_set & ((uint64_t)1 << (i << 1))) >> i; } return ans; } ``` ##### 2. 延伸題意,將重複 3 次改為改為重複 4 次或 5 次,並找出可容易推廣到多次的程式碼; 詳細說明可參考[討論區](https://leetcode.com/problems/single-number-ii/discuss/43295/Detailed-explanation-and-generalization-of-the-bitwise-operation-method-for-single-numbers),概念上與此題的解法相似,持續相加出現的 bit,直到出現的 bit 到達上限 `k`,將之移除。而 `k` 為每個數字出現的上限,可以知道,要記錄 `k` 個數字的 bit,必須至少要有 $\lceil\log_{2}k\rceil$ 個變數才行。 這邊 `k` 表示同樣數字出現的次數,以下為 `k` 任意數時迴圈內的程式碼: ```c /* k == 2 */ x2 ^= x1 & i; x1 ^= i; mask = ~(x1 & x2); x2 &= mask; x1 &= mask; /* k == 3 */ x3 ^= x2 & x1 & i; x2 ^= x1 & i; x1 ^= i; mask = ~(x1 & ~x2 & x3); x3 &= mask; x2 &= mask; x1 &= mask; /* k == n */ xn ^= xn-1 & xn-2 ... & x1 & i; xn-1 ^= xn-2 & xn-3 ... & x1 & i; ... x1 ^= i; mask = ~(x1 & x2 ... & xn); xn &= mask; xn-1 &= mask; ... x1 &= mask; ``` 要注意上面 `mask` 的值只是參考,實際上要隨著 `k` 值來決定 True 或 False,用來處理當 `xn, xn-1, ..., x1` 在某個 bit 上的值等於 `k` 時的情形,將這些 bit 清空。 假設表格內 `x5, x4, ..., x1` 都為 1 bit 的值,mask 的計算如下: | k | $x_{5}$ | $x_{4}$ | $x_{3}$ | $x_{2}$ | $x_{1}$ | mask | | --- | ------- | ------- | ------- | ------- | ------- | -------------------- | | 1 | False | False | False | False | True | ~(x1) | | 4 | False | False | True | False | False | ~(x3) | | 7 | False | False | True | True | True | ~(x3 & x2 & x1) | | 14 | False | True | True | True | False | ~(x4 & x3 & x2) | | 29 | True | True | True | False | True | ~(x5 & x4 & x3 & x1) | 擴展到 32 bits 的 integer,當所有變數內的某個位置的 bit 與 `k` 的位元相符合時,表示已經達到上限了,將之從所有變數內刪除(NOT + AND)。 接著就是將這些步驟擴充成能隨著 `k` 來改變,底下程式碼邏輯與上面完全相同,只是用到了迴圈來解決變數數量不同的問題。 ```c int singleNumber(int* nums, int len, int k) { uint32_t num_variable = ceil(log2(k)); //c99 uint32_t mask; int* x = calloc(num_variable, sizeof(int)); for (int i = 0; i < len; ++i) { mask = 0xFFFFFFFF; for (int v = 0; v < num_variable; ++v) { int tmp = nums[i]; for (int j = v + 1; j < num_variable; ++j) { tmp &= *(x + j); } *(x + v) ^= tmp; } for (int v = 0; v < num_variable; ++v) { mask &= ((1u << v) & k) > 0 ? *(x + v) : ~*(x + v); } mask = ~mask; for (int v = 0; v < num_variable; ++v) { *(x + v) &= mask; } } int ans = 0; for (int v = 0; v < num_variable; ++v) ans |= *(x + v); free(x); return ans; } ``` :::info 記得使用到 `math.h` 編譯時要輸入 `-lm` 連結至函式庫 ::: ###### tags: `linux2020`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully