我的選擇方向大概是:
另外也可以參考胰島素指數選擇比較優良的種類 4
而近年來搜尋引擎提供的內容逐漸被各種SEO後的網站取代。第一頁會看到的搜尋結果很大部分是內容農場,或是吸引點擊的假Blog(內容不一定實用,最終目的是導向你購買他們的商家服務)。
資訊逐漸從透過網頁呈現,轉變為透過影片呈現。顯然對創作者而言,影片是更有經濟效益的。既能帶來廣告收益,又可以減少被內容農場剽竊的狀況。
舉例來說,我想要搜尋哪些堅果對生酮最有幫助,搜尋引擎的結果仍然需要我從十幾個資訊來源中,篩選掉毫無幫助又落落長的文章,並從多個資訊源中比對共通的推薦種類。影片則精準的排名提出有哪些堅果最為推薦。
又或者是如何學習英文,比方說透過看美劇練習聽力和口說的方式,影片創作者更能提供令人信服的論文研究做為參考,而網頁內容多半只是一些沒有用的罐頭資訊。
當然影片也不是全無壞處。為了湊時間(可能是平台上的規定或最佳化),多半在開頭結尾會有很多無用內容,或是塞了大量的廢話拖時間。有時好心的觀眾會在留言標註出重點或時間軸方便快轉,就能節省不少的時間。
如果搜尋引擎2.0能夠處理影片內容,並快速摘錄出精華,應該可以迅速地取代掉現有的搜尋引擎巨頭。由於LLM的出現,我相信這種革命性的轉變已經不遠了。
獲利方式或許也會變成付費搜尋,畢竟啟動LLM運算的成本比傳統的搜尋大得多了,除非有甚麼方式可以降低成本,才可能繼續透過廣告業務獲利。
]]>Go對於太靈活的資料有某種先天適應不良,當然你也可以說是API設計者的問題。讓我們看這個例子:
{
"src": "us-west-1",
"TW": 1.5,
"JP": 3,
"HK": false,
"US": null,
}
其中src
會固定存在,但底下的country code和數值是動態的。顯然你沒有辦法用struct來當成unmarshal載體。當你選擇用map時,則會面臨各種type assertion的額外程式碼。可以說當你選擇了map[string]any
時,相關的程式碼處理就註定了不會太簡潔。
有如PHP之於Laravel、Python之於flask。Go還沒有一個主宰市場的框架存在。我也在持續的關注開發圈的動態,但似乎還沒有一款穩定成熟的框架,是在職缺描述上直接指定要熟悉它的。當然這並不是壞事,但也代表不同團隊可能有自己的風格和規範,在上手時需要花時間在這方面。
官方對於Package Naming希望你以功能為主軸出發,而不是無意義的common name。舉例來說如果你有一個models package,裡面有user和order兩個struct,分別被不同的controller使用。實際上這兩個struct並沒有一定要放在一起的必要,因此models的劃分方式只是單純的進行分類,而沒有考量程式碼間的聚合力。或許你會做出models/user
,但這也代表你非常有可能再出現一個controllers/user
,這時候下游如果要import時就會遇到renaming的困擾。那何不將其合併為user
就好呢?
當你這樣做之後,在package上便有domain劃分的問題。舉例來說我想要新增個人資料管理和業務業績管理的功能,我想你顯然不會把這些功能都塞進user
裡面,否則user
最終會成為一個God-like package。如果你選擇新增兩個package叫personal
和sales
的話,這時候可能發生的狀況是:
前端同時想要在list user時知道他是不是業務 and 想要在list業務時帶出他的名字:由於你只能從sales
import user
或從user
import sales
,因此這變成無法兩全的狀況。所以你可能想要新增一個下游包controller
去同時import user
和sales
,並透過某些方式處理資料:
controller
將對方的實作以interface的方式送進去controller
層呼叫兩方並自己組合資料user
和sales
struct放在一起 (這回到了剛才提到的models問題)這兩種方式都無可避免的,需要額外的程式碼來處理這個狀況。而第三種方式是最簡單也下意識會使用的,然而他很容易打破domain間的藩籬並為程式碼架構帶來混亂。
當我們想要遵守前面的Package規範來處理程式碼的聚合力(或者說邊界)時,架構的設計會更複雜。就以最流行的MVC來說,你可能下意識的定義了models
、controllers
、views
。而當程式碼成長到某個規模才會發現邊界不明確帶來的困擾。如果你有意識的想進行切分,這就像是將每個package都假想為一個microservice進行DDD,沒有切在正確的維度會導致Package間有大量的交互,使得前面提到的額外的程式碼
會非常多。
此外,記憶體對齊(memory alignment)是另一個關鍵因素。為了獲得更高的存取效率,記憶體地址需要按照一定的邊界對齊。例如,在 64 位元 CPU 上,記憶體地址會對齊到 8 Bytes(即 64 位元)邊界。這導致記憶體分配器不會分配像 0x12341111 這樣的地址,而會分配像 0x12341110 這樣對齊的地址。因此,在 64 位元 CPU 上,地址的最低 3 位元(對應於 8 Bytes對齊)不會被使用。這樣的對齊策略進一步提升了記憶體存取的效率,並為指標上的資料標記提供了可能性。
這適用於某些特殊的場景,例如:
https://muxup.com/2023q4/storing-data-in-pointers https://github.com/golang/go/blob/master/src/runtime/tagptr_64bit.go
]]>對於工作中可能出現的任務,這邊有幾點建議:
作為一名軟體工程師,你的工作遠遠超出了單純的程式碼撰寫。從需求分析到設計、實作、測試、部署,再到最終的維護和溝通,每一步都是創造出高品質軟體的重要組成部分。在這個過程中,你不僅是技術的實踐者,更是問題解決者和創新的推動者。記住,持續學習和適應變化是這個行業不變的規則,而你的熱情和創造力將是你職業生涯中最強大的資產。
]]>Jump Table (或Branch Table)是一種程式撰寫技巧,它和Lookup Table類似,都是藉由Table儲存結果的方式來減少計算量或分支程式碼(if-else, switch-case)。不同之處在於Jump Table的結果儲存的是function pointer,而Lookup Table儲存的是值。
Table可以用array或是map的方式實作。array的實作通常會受限於其輸入的值域,當輸入的範圍是1-100時,可以簡單地宣告一個長度100的陣列來儲存。如果輸入的範圍可能是1-10000000時,使用同樣的方式會相當浪費空間。如果輸入的範圍很大,但實際會出現的值,其集合相當小(例如只會出現1, 10, 100, 1000, 10000),這時我們可能需要仔細分析是否能找到一個function壓縮輸入範圍到可接受的程度。 但map的存取時間複雜度會更高,因此使用map時更需要仔細橫量是否真的有得到更好的結果。
大部分的情況下使用if-else或switch-case在可讀性上會比較友好,但Table相對來說有可能換來較好的效能。因為我們可以藉由它消除branch來避免pipeline hazard。而在無法使用branch的場合,例如:用SIMD指令集實作時,這也成為一個常用的技巧。
接下來我們看幾個套用Jump Table技巧的例子
這是最簡單的用法,使用map來處理任意的key值。需要注意的是他的存取時間不是O(1),再加上cache data locality問題,因此在效能最佳化上有可能會打折扣。
inline CurrencyType toCurrency(const std::string &c)
{
static std::map<std::string, CurrencyType> currencies = {
{"NTD", CurrencyType::NTD}, {"TWD", CurrencyType::NTD},
{"USD", CurrencyType::USD}, {"EUR", CurrencyType::EUR},
{"JPY", CurrencyType::JPY}, {"GBP", CurrencyType::GBP},
{"AUD", CurrencyType::AUD}, {"HKD", CurrencyType::HKD},
{"RMB", CurrencyType::RMB}, {"ZAR", CurrencyType::ZAR},
{"KRW", CurrencyType::KRW}, {"SGD", CurrencyType::SGD},
{"CAD", CurrencyType::CAD}, {"SEK", CurrencyType::SEK},
{"CHF", CurrencyType::CHF}, {"NZD", CurrencyType::NZD},
{"THB", CurrencyType::THB}, {"PHP", CurrencyType::PHP},
{"IDR", CurrencyType::IDR}, {"MYR", CurrencyType::MYR},
{"VND", CurrencyType::VND}, {"CNY", CurrencyType::RMB},
};
if(auto it = currencies.find(c); it != currencies.end()) {
return it->second;
}
return CurrencyType::MAX;
}
定點數相較於浮點數,它使用整數來模擬實數。我們需要定義一個整數中有哪些部分用來表示小數點,假設要使用一個32位整數,並且他有3位小數點,那可表示的範圍就是2147483.647 ~ -2147483.648
。實際的使用場景出現在貨幣或價格的計算上,我們可以預期貨幣或價格的小數點是有限且固定的,而且不希望有浮點數的誤差問題,就會使用這種特殊的數字表示方式。
以下列的程式碼為例,在class內部約定儲存時固定有4位的小數點。value()需要根據輸入的參數給出固定有N位小數點時的定點數結果。因此decimalLocator為4時應該直接輸出,為0時應該除以10000來消除所有小數再輸出,以ChatGPT提供的實作大致如下:
class FixedPointNumber {
private:
int64_t value_;
public:
FixedPointNumber(int64_t value) : value_(value) {}
int64_t value(int8_t decimalLocator = 4) const {
if (decimalLocator < 0 || decimalLocator > 4) {
std::cerr << "Invalid decimalLocator value. It should be in the range of 0 to 4." << std::endl;
return 0;
}
int64_t scale = 1;
for (int i = 0; i < decimalLocator; ++i) {
scale *= 10;
}
return value_ / scale;
}
};
在這裡他使用了一個迴圈和多次乘法來實作功能,如果改用Lookup Table的話結果如下:
class FixedPointNumber {
public:
inline int64_t value(uint8_t decimalLocator = 4) const
{
assert(0 <= decimalLocator && decimalLocator <= 4);
return value_ / decimalShiftTable_[decimalLocator];
}
private:
static inline int64_t decimalShiftTable_[5] = {10000, 1000, 100, 10, 1};
int64_t value_{};
我們可以藉此消除迴圈和乘法。
如果有一個判斷輸入並執行特定動作的功能,通常我們可以用if或switch來實作,如下:
int main() {
int choice;
// 請使用者輸入 0、1、2、3 中的一個數字
std::cout << "請輸入 0 表示向上,1 表示向下,2 表示向左,3 表示向右:";
std::cin >> choice;
// 使用跳躍表來呼叫對應的函數
switch (choice) {
case 0:
up();
break;
case 1:
down();
break;
case 2:
left();
break;
case 3:
right();
break;
default:
std::cout << "無效的選擇" << std::endl;
break;
}
return 0;
}
你可以使用一個array來實作Table,以簡化這段程式碼:
int main() {
int choice;
// 請使用者輸入 0、1、2、3 中的一個數字
std::cout << "請輸入 0 表示向上,1 表示向下,2 表示向左,3 表示向右:";
std::cin >> choice;
// 定義一個函數指針陣列,將輸入值映射到函數
std::function<void()> functions[] = {up, down, left, right};
if (choice >= 0 && choice < 4) {
// 如果輸入值有效,則呼叫相應的函數
functions[choice]();
} else {
std::cout << "無效的選擇" << std::endl;
}
return 0;
}
在這個case中很幸運的是輸入的值非常小,因此你的array長度只有4。當輸入為(10, 20, 30, 70, 90)
的時候這樣做可能不會帶來比較好的結果,其一是浪費空間,其二是破壞了cache的data locality可能導致較差的效能。
要應對這樣的case,我們還可以透過bitwise的方式暴力尋找特徵。簡單的說就是尋找一個function讓(10, 20, 30, 70, 90)
可以對應(0-3)
或(0-7)
或(0-15)
。這三種範圍分別對應了從64bits中選擇2、3、4bits。然而實際範圍可能不會是64bits,以上面為例,90則代表有效範圍是7bits,更大的bit都是0。
透過如下的程式可以幫我們暴力找出所有可行的組合
#include <algorithm>
#include <bitset>
#include <cstdio>
#include <iostream>
#include <set>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
// uint8_t inputs[] = {10, 30, 41, 50, 102, 103, 104, 105, 112, 114};
// uint8_t inputs[] = {10, 30, 41, 50};
// uint8_t inputs[] = {102, 103, 104, 105, 112, 114};
uint8_t inputs[] = {'3', '4', 'A', 'B', 'C', 'D', 'E'};
size_t length = sizeof(inputs);
constexpr size_t bits_size = sizeof(inputs[0]) * 8;
vector<size_t> find_indexes()
{
vector<size_t> result;
for(size_t i = 0; i < bits_size; i++) {
bool allSame = true;
bitset<bits_size> bs1(inputs[0]);
for(int j = 1; j < length; j++) {
bitset<bits_size> bs2(inputs[j]);
if(bs1[i] != bs2[i])
allSame = false;
}
if(allSame == false) {
result.push_back(i);
}
}
return result;
}
void show_bitmask(string bitmask)
{
bitset<bits_size> bs;
cout << "============" << endl;
cout << "bitmask: ";
for(size_t i = 0; i < bitmask.size(); i++) {
if(bitmask[i]) {
cout << i << " ";
bs[i] = true;
}
}
cout << endl;
cout << "binary form: " << bs << endl;
for(int i = 0; i < length; i++) {
stringstream ss;
bitset<bits_size> bs(inputs[i]);
bitset<bits_size> nbs;
int k = 0;
for(int j = 0; j < bitmask.size(); j++) {
ss << "[" << j << ":" << bs[j] << "]: ";
if(bitmask[j]) {
nbs[k] = bs[j];
ss << bs[j];
k++;
}
else {
ss << "X";
}
ss << " ";
}
cout << i << ": " << inputs[i] << "\t";
cout << ss.str() << "\t" << nbs << "\t" << nbs.to_ulong();
cout << endl;
}
}
int main()
{
printf("lookup lut for bit_size: %ld in the following set\n", bits_size);
for(size_t i = 0; i < length; i++) {
bitset<bits_size> bs(inputs[i]);
cout << i << ": " << int(inputs[i]) << "\t" << bs << endl;
}
auto indexes = find_indexes();
cout << "index candidates: [";
for(const auto &idx : indexes) {
cout << idx << ",";
}
cout << "]" << endl;
auto maxElement = *max_element(indexes.begin(), indexes.end());
cout << "max element: " << maxElement << endl;
for(int i = 1; i < indexes.size(); i++) {
auto max = 1ULL << i;
// cout << "use " << i << "bits has range 0~" << max - 1 << endl;
if(max < length) {
cout << i << " bits can't presents set size " << length << endl;
continue;
}
cout << "try use " << i << " bits" << endl;
const auto K = i;
const auto N = maxElement;
std::string bitmask(K, 1); // K leading 1's
bitmask.resize(N, 0); // N-K trailing 0's
do {
set<size_t> exists;
// check every input transform to picked bits not conflicts.
for(int i = 0; i < length; i++) {
size_t v = 0;
// pick bits.
for(int j = 0; j < N; ++j) // [0..N-1] integers
{
// if (bitmask[i]) std::cout << " " << i;
if(bitmask[j])
v |= inputs[i] & (1 << j);
}
exists.insert(v);
}
if(exists.size() == length) {
show_bitmask(bitmask);
return 0;
}
} while(std::prev_permutation(bitmask.begin(), bitmask.end()));
}
cout << "no result found" << endl;
return 0;
}
實際案例如下,我們想將(10, 30, 41, 50)
放到Table,經過bitwise操作提取其中的3個bits以後,就可以把array長度壓縮到8了。
static HandleFnPtr table[8] = {
/* 0 */ &ClientConnection::noop,
/* 1 */ &ClientConnection::handle50,
/* 2 */ &ClientConnection::noop,
/* 3 */ &ClientConnection::noop,
/* 4 */ &ClientConnection::handle41,
/* 5 */ &ClientConnection::handle10,
/* 6 */ &ClientConnection::noop,
/* 7 */ &ClientConnection::handle30,
};
inline size_t CodeToIndex(uint32_t code)
{
// use 3 bits.
#define toIndex(x) ((((x)&0x0E) >> 1) & 0x07)
static_assert(toIndex(10) == 5);
static_assert(toIndex(30) == 7);
static_assert(toIndex(41) == 4);
static_assert(toIndex(50) == 1);
return toIndex(code);
#undef toIndex
}
文中提及了三個使用Table加速技巧的案例,這是在利用SIMD撰寫加速演算法時常用的技巧,像是base64編碼解碼就可以用LUT方法加速。除此之外他也可以應用來簡化程式碼或預先建表加速計算。
]]>Too many authentication failures
這是因為預設啟用了對所有Host都使用ssh-agent
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
目前看起來解決方法只有config要辛苦一點列舉了
Host github.com
HostName github.com
User git
IdentityFile "~/.ssh/github.pub"
IdentitiesOnly yes
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
Host *
IdentitiesOnly yes
https://developer.1password.com/docs/ssh/agent/advanced/#ssh-server-six-key-limit
]]>天鋇R1 N100是最近才推出的新產品,規格和我理想中的很接近:
加上機殼設計不錯,性價比輾壓自組。
淘寶售價是899元人民幣,加上運費大約50元人民幣。大約4400元新台幣入手。
開箱本體,寬高約一個手掌大。只有附變壓器和電源線。
買的是準系統,RAM和SSD要自己加裝。拆開也很簡單,只要卸下底部四顆螺絲,說明書也有教學。
把硬碟tray拉開以後就可以安裝了。
RAM買的是16G,方便之後有其他用途。
SSD買gen3 2T,反正不會存重要資料可以隨便買。
HDD買礦盤來用,省錢至上。
這台的os我還是選擇裝在SD卡上,但他的退卡機制貌似因為機殼問題不太順暢,要拿鑷子又戳又夾的,十分勉強。建議可以的話不要像我這樣搞。
硬碟Tray很爛,大力出奇蹟硬掰開安裝的那種,有點無言。
安裝完成,藏在電視後面剛剛好。
作業系統選擇裝ubuntu server版,上面用docker啟動qbittorrent。簡單的用途還是敲指令了事。
]]>溝通能力是工程師往資深邁進重要的一個能力,而公開演講作為一種廣播式通訊機制,精進這項能力可以讓多數人理解認同你的想法。試想你的主管開了三十分鐘的會議,令人昏昏欲睡,簡直是在浪費時間。
以下有請ChatGPT給出一個示範:
問題(Problem): 企業面臨的網路安全威脅日益增多,傳統的安全邊界已不足以保護企業內外部散布的資料和應用系統。
激化(Agitate): 如果繼續依賴過時的安全模型,企業不僅容易受到資料洩露和駭客攻擊的風險,而且可能因為不符合新的合規要求而面臨法律與財務上的後果。
解決方案(Solution): Cloudflare Zero Trust 安全解決方案透過確保所有用戶和裝置在存取企業資源前都必須通過嚴格的身份驗證和行為分析,提供了一個更為安全和符合現代企業需求的網路環境,大幅減少安全威脅和遵規風險。
Before(之前狀態): 傳統安全模型依賴於固定的網絡邊界來保護資源,但隨著雲端技術和遠端工作的普及,這種模型無法有效保護分散在各處的企業資源。
After(之後狀態): 採用Cloudflare Zero Trust 安全架構,企業能夠確保每一次的存取都經過嚴格的身份驗證和授權,無論用戶或資源位於何處,都能實現精確的安全控制。
Bridge(橋接方案): 介紹Cloudflare Zero Trust 如何連接「之前」和「之後」的狀態,包括它的主要特點,比如最小權限原則、身份和設備的持續性驗證,以及自動化的安全政策實施,這些都是幫助企業從傳統安全模型過渡到現代安全架構的關鍵。
主題(Topic): Cloudflare Zero Trust 架構
為何重要(Why it matters): 隨著遠端工作模式的興起與網路威脅的增加,傳統的安全防護模型已不足以應對現今複雜的網絡環境。Cloudflare Zero Trust 提供了一個不依賴於傳統網絡邊界的安全架構,確保只有驗證和授權的用戶及裝置才能存取資源。
信息(Information):
總結(Recap): 總結Cloudflare Zero Trust 如何幫助企業面對當代網絡安全挑戰,並提供一個靈活、安全且易於管理的網絡存取解決方案。
]]>在職涯發展上通常分為對人的管理和對技術的管理,這是因為把最好的工程師擺到管理職並不一定是個好的決定。
從senior繼續往上走才會面臨二轉(team lead or tech lead)。
staff engineer is a.k.a principal engineer or Tech Lead.
staff engineer和senior engineer的差別是技術方面的職責,但仍然是一個individual contributer。沒有對人的管理責任。
而staff engineer通常的期待是:
當一個組織成長到engineering manager在技術方面無法滿足時,就需要staff engineer提供相關的意見,來補足缺乏的部分。所以他並不是一個必要的角色。
senior staff engineer則是能夠勝任更大型組織的technical leadership。
]]>