天天看點

GFlags使用總結

Github  

官方文檔

Gflags簡明教程 http://dreamrunner.org/blog/2014/03/09/gflags-jian-ming-shi-yong/

GFlags使用文檔 http://www.yeolar.com/note/2014/12/14/gflags/

簡介

GFlags是Google開源的一套指令行參數處理的開源庫,包括C++的版本和python 版本。和 getopt() 之類的庫不同,flag的定義可以散布在各個源碼中,而不用放在一起。一個源碼檔案可以定義一些它自己的flag,連結了該檔案的應用都能使用這些flag。這樣就能非常友善地複用代碼。如果不同的檔案定義了相同的flag,連結時會報錯。

定義Flags

定義一個flag是簡單的:隻需要使用你想用的類型的相應的宏就可以。

使用flags需要包含頭檔案 :

example:

// foo.cc
#include <gflags/glags.h>

DEFINE_bool(big_menu, true, "Include 'advanced' options in the menu listing");
DEFINE_string(languages, "english,french,german",
              "comma-separated list of languages to offer in the 'lang' menu");
           

示例代碼分别定義了一個bool和一個string類型的參數,該宏的三個參數含義分别為指令行參數名,參數預設值,以及參數的幫助資訊。gflag不支援清單,使用者通過靈活借助string參數實作,比如上述的languages參數,可以類型為string,但可看作是以逗号分割的參數清單。

支援的類型:

  • DEFINE_bool: boolean
  • DEFINE_int32: 32-bit integer
  • DEFINE_int64: 64-bit integer
  • DEFINE_uint64: unsigned 64-bit integer
  • DEFINE_double: double
  • DEFINE_string: C++ string

DEFINE宏包含三個參數:flag名、預設值、描述方法的幫助。幫助會在執行 --help flag時顯示。

可以在任何源檔案中定義flag,但是每個隻能定義一次。如果需要在多處使用,那麼在一個檔案中 DEFINE ,在其他檔案中 DECLARE 。比較好的方法是在 .cc 檔案中 DEFINE ,在 .h 檔案中 DECLARE ,這樣包含頭檔案即可使用flag了。

使用flag通路參數

當參數被定義後,通過FLAGS_name就可通路到對應的參數,定義的flag可以像正常的變量一樣使用,隻需在前面加上 FLAGS_字首。如前面例子中的定義了 FLAGS_big_menu 和 FLAGS_languages兩個變量。可以像一般變量一樣讀寫:

if (FLAGS_consider_made_up_languages)
  FLAGS_languages += ",klingon";   // implied by --consider_made_up_languages
if (FLAGS_languages.find("finnish") != string::npos)
  HandleFinnish();
           

以上的通路方式,僅在參數定義和通路在同一個檔案(或是通過頭檔案包含)時,FLAGS_name才能通路到參數,如果要通路其他檔案裡定義的參數,則需要使用DECLARE_type。比如在foo.cc中DEFINE_string(color, “red”, “the color you want to use”); 這時如果你需要在foo_test.cc中使用color這個參數,你需要加入DECLARE_string(color, “red”, “the color you want to use”);

參數檢查

定義參數後,可以給參數注冊一個檢查函數(validator),當從指令行指定參數或通過SetCommandLineOption()指定參數時,檢查函數就會被調用,兩個參數分别為指令行參數名,以及設定的參數值

static bool ValidatePort(const char* flagname, int32 value) { 
   if (value > 0 && value < 32768)   // value is ok 
     return true; 
   printf("Invalid value for --%s: %d\n", flagname, (int)value); 
   return false; 
} 
DEFINE_int32(port, 0, "What port to listen on"); 
static const bool port_dummy = RegisterFlagValidator(&FLAGS_port, &ValidatePort)
           

建議在定義參數後,立即注冊檢查函數。RegisterFlagValidator()在檢查函數注冊成功時傳回true;如果參數已經注冊了檢查函數,或者檢查函數類型不比對,傳回false。

初始化所有參數

在引用程式的main()裡通過 google::ParseCommandLineFlags(&argc, &argv, true); 即完成對gflags參數的初始,其中第三個參數為remove_flag,如果為true,gflags會移除parse過的參數,否則gflags就會保留這些參數,但可能會對參數順序進行調整。 比如 “/bin/foo” “arg1” “-q” “arg2” 會被調整為 “/bin/foo”, “-q”, “arg1”, “arg2”,這樣更好了解。

#include <iostream>
#include <gflags/gflags.h>
 
DEFINE_bool(isvip, false, "If Is VIP");
DEFINE_string(ip, "127.0.0.1", "connect ip");
DECLARE_int32(port);
DEFINE_int32(port, 80, "listen port");
 
int main(int argc, char** argv)
{
  google::ParseCommandLineFlags(&argc, &argv, true); //初始化所有參數
  std::cout<<"ip:"<<FLAGS_ip<<std::endl;
  std::cout<<"port:"<<FLAGS_port<<std::endl;
  if (FLAGS_isvip)
  {
      std::cout<<"isvip:"<<FLAGS_isvip<<std::endl;
  }
  google::ShutDownCommandLineFlags();
  return 0;
}
           

連結時使用 -gflags ,運作使用 ./gflags -ip=“211.152.52.106” -port=8080 -isvip=true ,但是很遺憾,使用 valgrind 檢測有記憶體洩漏。

輸出結果如下:

ip:211.152.52.106
port:8080
isvip:1
           

在指令行指定參數

比如要在指令行指定languages參數的值,可通過如下4種方式,int32, int64等類型與string類似。

app_containing_foo --languages=“chinese,japanese,korean”

app_containing_foo -languages=“chinese,japanese,korean”

app_containing_foo --languages “chinese,japanese,korean”

app_containing_foo -languages “chinese,japanese,korean”

對于bool類型,則可通過如下幾種方式指定參數

app_containing_foo --big_menu

app_containing_foo --nobig_menu

app_containing_foo --big_menu=true

app_containing_foo --big_menu=false

特殊參數

–help 列印定義過的所有參數的幫助資訊

–version 列印版本資訊 通過google::SetVersionString()指定

–nodefok 但指令行中出現沒有定義的參數時,并不退出(error-exit)

–fromenv 從環境變量讀取參數值 --fromenv=foo,bar表明要從環境變量讀取foo,bar兩個參數的值。通過export FLAGS_foo=xxx; export FLAGS_bar=yyy 程式就可讀到foo,bar的值分别為xxx,yyy。

–tryfromenv 與–fromenv類似,當參數的沒有在環境變量定義時,不退出(fatal-exit)

–flagfile 從檔案讀取參數值,–flagfile=my.conf表明要從my.conf檔案讀取參數的值。在配置檔案中指定參數值與在指令行方式類似,另外在flagfile裡可進一步通過–flagfile來包含其他的檔案。

Caffe執行個體解析

以 tools/caffe.cpp 為例分析gflags使用方法

在tools/caffe.cpp中首先包含了gflags的頭檔案

指令行參數傳遞

接下來定義了一些需要從指令行傳遞參數的變量

DEFINE_string(gpu, "",
    "Optional; run in GPU mode on given device IDs separated by ','."
    "Use '-gpu all' to run on all available GPUs. The effective training "
    "batch size is multiplied by the number of devices.");
DEFINE_string(solver, "",
    "The solver definition protocol buffer text file.");
DEFINE_string(model, "",
    "The model definition protocol buffer text file.");
DEFINE_string(phase, "",
    "Optional; network phase (TRAIN or TEST). Only used for 'time'.");
DEFINE_int32(level, 0,
    "Optional; network level.");
......
           

在main函數中調用的GlobalInit()函數中調用了ParseCommandLineFlags()

// Google flags.
::gflags::ParseCommandLineFlags(pargc, pargv, true);
           

這個函數的作用就是解析指令行參數。

pargc,pargv為指令行傳遞的參數個數和參數表,第三個參數作用為:

作用
true 函數處理完成後,argv中隻保留argv[0],argc會被設定為1
false argv和argc會被保留,但是注意函數會調整argv中的順序。

執行完這參數後,就可以用FLAGS_xxx通路變量了。

例如caffe.cpp中, FLAGS_solve表示指令行參數傳遞的模型檔案,FLAGS_weights表示令行參數傳遞的權值檔案

// Train / Finetune a model.
int train() {
  CHECK_GT(FLAGS_solver.size(), 0) << "Need a solver definition to train.";
  CHECK(!FLAGS_snapshot.size() || !FLAGS_weights.size())
      << "Give a snapshot to resume training or weights to finetune "
      "but not both.";
           

幫助資訊

gflags::SetUsageMessage函數用于設定指令行幫助資訊。

// Usage message.
  gflags::SetUsageMessage("command line brew\n"
      "usage: caffe <command> <args>\n\n"
      "commands:\n"
      "  train           train or finetune a model\n"
      "  test            score a model\n"
      "  device_query    show GPU diagnostic information\n"
      "  time            benchmark model execution time");
           

設定幫助資訊後,當參數錯誤或加 -help選項是可以列印幫助資訊

./build/tools/caffe
caffe: command line brew
usage: caffe <command> <args>
 
commands:
  train           train or finetune a model
  test            score a model
  device_query    show GPU diagnostic information
  time            benchmark model execution time
...
           

版本資訊

gflags::SetVersionString用于設定版本資訊

// Set version
  gflags::SetVersionString(AS_STRING(CAFFE_VERSION));
           

這裡的CAFFE_VERSION是在Makefile中定義的

當指令行參數加 -version 時會列印版本資訊

./build/tools/caffe -version
           

caffe version 1.0.0-rc3

demo

//demo_gflags.cc
#include "gflags/gflags.h"
DEFINE_string(server_ip, "127.0.0.1", "server ip");
DEFINE_int32(server_port, 8080, "server port");
           
// demo.cc
#include <iostream>
#include "gflags/gflags.h"

DEFINE_bool(test_bool, false, "test bool value");
DEFINE_int32(test_int32, 666, "test int value");

DECLARE_string(server_ip);
DECLARE_int32(server_port);

int main(int argc, char** argv) {
  google::ParseCommandLineFlags(&argc, &argv, true);

  // for (int i = 0; i < argc; i++) {
  //   std::cout << argv[i] << std::endl;
  // }

  if (FLAGS_test_bool) {
    std::cout << "test_bool is true." << std::endl;
    std::cout << "test_int32: " << FLAGS_test_int32 << std::endl;
  }

  std::cout << "server ip: " << FLAGS_server_ip
            << " , server port: " << FLAGS_server_port << std::endl;

  return 0;
}
           
// CMakeLists.txt
cmake_minimum_required(VERSION 3.5.1)
project(gflag)

find_package(gflags REQUIRED)

add_executable(${PROJECT_NAME}_demo demo.cc demo_gflags.cc)
target_link_libraries(${PROJECT_NAME}_demo ${GFLAGS_LIBRARIES})
           
//demo.gflags
-server_ip="192.168.0.123"
-server_port=8899
           
編譯
$ cd gflag
$ mkdir build
$ cmake ..
$ make
           
執行
# 不加指令行
$ ./gflag_demo
# 加指令行變量控制 `-test_bool=true` test_bool為程式開頭定義的标志
$ ./gflag_demo -test_bool=true -test_int32=666
# 加指令行檔案輸入 `-flagfile=filename` 注意路徑, - 和 -- 一樣
$ ./gflag_demo -flagfile=../demo.gflags ##檔案絕對路徑
# 其他
# -fromenv=value 從環境中讀取value
# --tryfromenv=value 環境中是否未定義不是緻命錯誤
           

gflags使用規範

任何好用的工具如果使用不當都會帶來不好的後果,gflags也是一樣。我遇到過一些gflags的“坑”,還從上司和同僚那裡獲得一些好的想法,整理成7條gflags使用規範。有意識的遵循這些規範,對項目的開發維護和自身的技術成長都将有很大的益處。

  • 規範1:bool類型的gflags預設值設定成false,防止誤啟用新功能。

    新的功能上線一定要經過代碼審查、測試和驗證流程,預設為true的gflags風險太大。

  • 規範2:應定時清理舊的gflags。

    随着時間的流逝,代碼裡的gflags會越來越多,當你的工程代碼裡包含成百上千行的gflags時,閱讀和維護代碼的體驗簡直是太過酸爽。非常有必要定時删除代碼中舊的gflags,根據其開關打開(true)和關閉(false)情況來删除gflags及其相關代碼。

  • 規範3:清理舊的gflags時應同時删除相應的gflags配置,以保證線上配置的整潔。

    配置檔案和代碼保持同步是一種非常好的開發和維護體驗。

  • 規範4:if語句中應盡量避免gflags參與邏輯運算。

    當if語句中出現與gflags相關的與、或、非邏輯運算時,事情就會變得複雜起來,gflags的開關狀态不再是唯一的決定因素,代碼閱讀和删除gflags也會變得十分困難。我曾經删錯過一個舊的gflags,幸運的是在CR階段(CodeReview,代碼審查)被細心的同僚指出,避免了一次踩坑。這是那段令我難忘的代碼的樣子:

    GFlags使用總結
    在這個例子中同時出現了與、或、非這3個邏輯運算,它是工程中真實存在的代碼,絕非由我杜撰。此時gflags a、b、c、d的值都是false,現在的任務是删除這些舊的gflags和它們包住的代碼,應該保留哪部分代碼呢?如果你和我一樣忘記了或運算||和與運算&&誰的優先級更高,那麼掉到坑裡的機率非常大。言歸正傳,我們有很多方法避免這樣的代碼出現,gflags絕不應該參與複雜的邏輯運算。
  • 規範5:公共子產品的gflags應盡快删除。

    公共子產品的gflags在上線運作一段時間後,應盡快删除,理想的情況是公共子產品都沒有gflags包含。這樣做的理由是使用公共子產品往往不知道這些gflags的存在,非常容易留坑。

  • 規範6:不要在單元測試代碼中使用gflags。

    如果UT(UnitTest,單元測試)代碼裡用到了gflags,情況會變得複雜,在删除舊的gflags時需要同步修改單元測試代碼,否則會導緻測試失敗,jenkins上的任務會變紅(即測試失敗)。是以,最好不要在單元測試代碼裡使用gflags。

  • 規範7:送出代碼時,應記錄新增或删除的gflags配置。

    這樣的好處是友善測試的同僚進行測試,這樣利人利己的規範是非常值得遵守的。

    最後,我把這7條規範總結并整理成一張圖檔,歡迎大家留言補充更多、更好的gflags使用規範。

    GFlags使用總結
  • 金句分享

人的天性之一,就是不會接受别人的批評,總是認為自己永遠是對的,喜歡找各種各樣的借口為自己辯解。

——出自《人性的弱點》,戴爾·卡耐基(Dale Carnegie),美國著名人際關系學大師。

解讀:永遠不要批評别人,因為指責隻不過是在浪費自己和他人的時間,應該換種方式去溝通和解決問題。

繼續閱讀