3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

makefile と build.ninja の比較。実アプリのビルド時間比較。 make から ninja への移行検討メモ

3
Last updated at Posted at 2019-10-22

これは何

GNU make から ninja に移行を検討中のメモです。
H.264 のリファレンスソフトウエアデコーダを題材に、 make と ninja のビルド時間を比較します。

単に makefile と build.ninja を見比べたい人は hello world の makefile と build.ninja.a ファイルのmakefile と build.ninja をどうぞご覧ください。

H.264 リファレンスデコーダって何?

H.264 という、一昔前のビデオ圧縮形式があります。
これのデコーダを開発する開発者や、圧縮アルゴリズムを検討したい研究者が利用することを想定したソフトウエアでソースコードが公開されています。
Linux 向けのビルド環境は素の makefile なので、これと ninja とのビルド時間を比較してみます。

想定読者

makefile を自分でかける人
.PHONY: clean を毎回書くのに飽き飽きした人
素の make の動作が遅いなぁと思う人
ninja 向けのメタビルドツールを作りたい人

結論

コマンド名 所要時間(秒)
ninja 17.547524
make 32.162396

ninja が圧倒的に早いです。ninja 公式が売りにしているとおり、早いです。
簡単な構成のプログラムであれば、build.ninja を直接書くのも全然ありと思います。
mesoncmake を使うと、記述量が減りますが、build.ninja の生成時間も気になりますんで。

測定環境

alpine linux on docker for windows 10
コンテナイメージは、下記 Dockerfile です。

Dockerfile
FROM gliderlabs/alpine:latest
RUN set -eux; \
	apk --update add --no-cache \
		libc-dev \
		gcc \
		make \
		tar \
		ccache \
		ninja
WORKDIR /root

このイメージで、 make と ninja を実施し、 powershell -C Measure-Command { cmd /C ninja.bat -C build } のように、 powershell で実行時間を測定しています。ninja.bat の中身は下記です。makeを試すときは、ninja を make に書き換えたファイルを使用しています。

ninja.bat
@echo off
setlocal
set docker_image=gcc
cd /d "%~dp0"
docker run --rm -it -v "%CD%":/root %docker_image% ninja %*
if errorlevel 1 pause

ファイル構成

jm19.zip を取得し、展開すると下記のディレクトリが作成されます。

JM/
|- bin
|- doc
|- JM.xcodeproj
|- lcommon
|- ldecod
|- lencod
|- rtp_loss
`- rtpdump

make での bin/ldecod.exe のビルド

カレントディレクトリを JM として、 make -C ldecod STC=1 を実行すると、今回の測定と同じオプションでビルドできます。

ninja での bin/ldecod のビルド

下記 build.ninja を JM/build.ninja に作成します。ビルドは、カレントディレクトリが JM とすると ninja となります。 JM/bin/ldecod が生成されます。

build.ninja
root = .
builddir = build
exedir = bin

# cc = ccache cc
cc = cc
cflags = -Wall -O3 -std=gnu99 -pedantic -ffloat-store -fno-strict-aliasing -fsigned-char
cppflags = -DNDEBUG -D__USE_LARGEFILE64 -D_FILE_OFFSET_BITS=64 -I$root/lcommon/inc -I$root/ldecod/inc
ldlibs = -lm -static

rule compile
     description = compiling object file "$out" ...
     deps    = gcc
     depfile = $out.d
     command = $cc -MMD -MP -MF $out.d $cflags $cppflags $target_arch -c -o $out $in

rule link
     description = creating binary "$out"
     command = $cc $ldflags $target_arch $in $loadlibes $ldlibs -o $out

build $exedir/ldecod: link $
  $builddir/blk_prediction.o $
  $builddir/config_common.o $
  $builddir/img_io.o $
  $builddir/img_process.o $
  $builddir/input.o $
  $builddir/io_raw.o $
  $builddir/io_tiff.o $
  $builddir/mbuffer_common.o $
  $builddir/memalloc.o $
  $builddir/mv_prediction.o $
  $builddir/nalucommon.o $
  $builddir/parsetcommon.o $
  $builddir/resize.o $
  $builddir/transform.o $
  $builddir/win32.o $
  $
  $builddir/annexb.o $
  $builddir/biaridecod.o $
  $builddir/block.o $
  $builddir/cabac.o $
  $builddir/configfile.o $
  $builddir/context_ini.o $
  $builddir/decoder_test.o $
  $builddir/dec_statistics.o $
  $builddir/erc_api.o $
  $builddir/erc_do_i.o $
  $builddir/erc_do_p.o $
  $builddir/errorconcealment.o $
  $builddir/filehandle.o $
  $builddir/fmo.o $
  $builddir/header.o $
  $builddir/image.o $
  $builddir/intra16x16_pred.o $
  $builddir/intra16x16_pred_mbaff.o $
  $builddir/intra16x16_pred_normal.o $
  $builddir/intra4x4_pred.o $
  $builddir/intra4x4_pred_mbaff.o $
  $builddir/intra4x4_pred_normal.o $
  $builddir/intra8x8_pred.o $
  $builddir/intra8x8_pred_mbaff.o $
  $builddir/intra8x8_pred_normal.o $
  $builddir/intra_chroma_pred.o $
  $builddir/intra_chroma_pred_mbaff.o $
  $builddir/intra_pred_common.o $
  $builddir/ldecod.o $
  $builddir/leaky_bucket.o $
  $builddir/loopFilter.o $
  $builddir/loop_filter_mbaff.o $
  $builddir/loop_filter_normal.o $
  $builddir/macroblock.o $
  $builddir/mbuffer.o $
  $builddir/mbuffer_mvc.o $
  $builddir/mb_access.o $
  $builddir/mb_prediction.o $
  $builddir/mb_read.o $
  $builddir/mc_direct.o $
  $builddir/mc_prediction.o $
  $builddir/nal.o $
  $builddir/nalu.o $
  $builddir/output.o $
  $builddir/parset.o $
  $builddir/quant.o $
  $builddir/read_comp_cabac.o $
  $builddir/read_comp_cavlc.o $
  $builddir/rtp.o $
  $builddir/sei.o $
  $builddir/transform8x8.o $
  $builddir/vlc.o

build $builddir/blk_prediction.o : compile $root/lcommon/src/blk_prediction.c
build $builddir/config_common.o : compile $root/lcommon/src/config_common.c
build $builddir/img_io.o : compile $root/lcommon/src/img_io.c
build $builddir/img_process.o : compile $root/lcommon/src/img_process.c
build $builddir/input.o : compile $root/lcommon/src/input.c
build $builddir/io_raw.o : compile $root/lcommon/src/io_raw.c
build $builddir/io_tiff.o : compile $root/lcommon/src/io_tiff.c
build $builddir/mbuffer_common.o : compile $root/lcommon/src/mbuffer_common.c
build $builddir/memalloc.o : compile $root/lcommon/src/memalloc.c
build $builddir/mv_prediction.o : compile $root/lcommon/src/mv_prediction.c
build $builddir/nalucommon.o : compile $root/lcommon/src/nalucommon.c
build $builddir/parsetcommon.o : compile $root/lcommon/src/parsetcommon.c
build $builddir/resize.o : compile $root/lcommon/src/resize.c
build $builddir/transform.o : compile $root/lcommon/src/transform.c
build $builddir/win32.o : compile $root/lcommon/src/win32.c

build $builddir/annexb.o : compile $root/ldecod/src/annexb.c
build $builddir/biaridecod.o : compile $root/ldecod/src/biaridecod.c
build $builddir/block.o : compile $root/ldecod/src/block.c
build $builddir/cabac.o : compile $root/ldecod/src/cabac.c
build $builddir/configfile.o : compile $root/ldecod/src/configfile.c
build $builddir/context_ini.o : compile $root/ldecod/src/context_ini.c
build $builddir/decoder_test.o : compile $root/ldecod/src/decoder_test.c
build $builddir/dec_statistics.o : compile $root/ldecod/src/dec_statistics.c
build $builddir/erc_api.o : compile $root/ldecod/src/erc_api.c
build $builddir/erc_do_i.o : compile $root/ldecod/src/erc_do_i.c
build $builddir/erc_do_p.o : compile $root/ldecod/src/erc_do_p.c
build $builddir/errorconcealment.o : compile $root/ldecod/src/errorconcealment.c
build $builddir/filehandle.o : compile $root/ldecod/src/filehandle.c
build $builddir/fmo.o : compile $root/ldecod/src/fmo.c
build $builddir/header.o : compile $root/ldecod/src/header.c
build $builddir/image.o : compile $root/ldecod/src/image.c
build $builddir/intra16x16_pred.o : compile $root/ldecod/src/intra16x16_pred.c
build $builddir/intra16x16_pred_mbaff.o : compile $root/ldecod/src/intra16x16_pred_mbaff.c
build $builddir/intra16x16_pred_normal.o : compile $root/ldecod/src/intra16x16_pred_normal.c
build $builddir/intra4x4_pred.o : compile $root/ldecod/src/intra4x4_pred.c
build $builddir/intra4x4_pred_mbaff.o : compile $root/ldecod/src/intra4x4_pred_mbaff.c
build $builddir/intra4x4_pred_normal.o : compile $root/ldecod/src/intra4x4_pred_normal.c
build $builddir/intra8x8_pred.o : compile $root/ldecod/src/intra8x8_pred.c
build $builddir/intra8x8_pred_mbaff.o : compile $root/ldecod/src/intra8x8_pred_mbaff.c
build $builddir/intra8x8_pred_normal.o : compile $root/ldecod/src/intra8x8_pred_normal.c
build $builddir/intra_chroma_pred.o : compile $root/ldecod/src/intra_chroma_pred.c
build $builddir/intra_chroma_pred_mbaff.o : compile $root/ldecod/src/intra_chroma_pred_mbaff.c
build $builddir/intra_pred_common.o : compile $root/ldecod/src/intra_pred_common.c
build $builddir/ldecod.o : compile $root/ldecod/src/ldecod.c
build $builddir/leaky_bucket.o : compile $root/ldecod/src/leaky_bucket.c
build $builddir/loopFilter.o : compile $root/ldecod/src/loopFilter.c
build $builddir/loop_filter_mbaff.o : compile $root/ldecod/src/loop_filter_mbaff.c
build $builddir/loop_filter_normal.o : compile $root/ldecod/src/loop_filter_normal.c
build $builddir/macroblock.o : compile $root/ldecod/src/macroblock.c
build $builddir/mbuffer.o : compile $root/ldecod/src/mbuffer.c
build $builddir/mbuffer_mvc.o : compile $root/ldecod/src/mbuffer_mvc.c
build $builddir/mb_access.o : compile $root/ldecod/src/mb_access.c
build $builddir/mb_prediction.o : compile $root/ldecod/src/mb_prediction.c
build $builddir/mb_read.o : compile $root/ldecod/src/mb_read.c
build $builddir/mc_direct.o : compile $root/ldecod/src/mc_direct.c
build $builddir/mc_prediction.o : compile $root/ldecod/src/mc_prediction.c
build $builddir/nal.o : compile $root/ldecod/src/nal.c
build $builddir/nalu.o : compile $root/ldecod/src/nalu.c
build $builddir/output.o : compile $root/ldecod/src/output.c
build $builddir/parset.o : compile $root/ldecod/src/parset.c
build $builddir/quant.o : compile $root/ldecod/src/quant.c
build $builddir/read_comp_cabac.o : compile $root/ldecod/src/read_comp_cabac.c
build $builddir/read_comp_cavlc.o : compile $root/ldecod/src/read_comp_cavlc.c
build $builddir/rtp.o : compile $root/ldecod/src/rtp.c
build $builddir/sei.o : compile $root/ldecod/src/sei.c
build $builddir/transform8x8.o : compile $root/ldecod/src/transform8x8.c
build $builddir/vlc.o : compile $root/ldecod/src/vlc.c

build.ninja 風の makefile でビルドを試す

標準のビルド結果と, ninja のビルド結果がバイナリ一致しなかったので、微妙にオプションがそろえられていなかったようです。なので、下記 makefile でもビルドしてみました。 JM/build_mk/makefile に配置しています。
make -C build_mk を実行すると、 JM/bin/ldecod.exe が生成されます。ビルド時間は 32.0608236 秒でした。標準の makefile とほぼ同じで、測定誤差程度の差といえるでしょう。

makefile
CFLAGS=-Wall -O3 -std=gnu99 -pedantic -ffloat-store -fno-strict-aliasing -fsigned-char -MMD -MP
CPPFLAGS=-DNDEBUG -D__USE_LARGEFILE64 -D_FILE_OFFSET_BITS=64 -I../lcommon/inc -I../ldecod/inc
LDLIBS=-lm -static

vpath %.c ../lcommon/src
vpath %.c ../ldecod/src

OBJS= \
  blk_prediction.o \
  config_common.o \
  img_io.o \
  img_process.o \
  input.o \
  io_raw.o \
  io_tiff.o \
  mbuffer_common.o \
  memalloc.o \
  mv_prediction.o \
  nalucommon.o \
  parsetcommon.o \
  resize.o \
  transform.o \
  win32.o \
  \
  annexb.o \
  biaridecod.o \
  block.o \
  cabac.o \
  configfile.o \
  context_ini.o \
  decoder_test.o \
  dec_statistics.o \
  erc_api.o \
  erc_do_i.o \
  erc_do_p.o \
  errorconcealment.o \
  filehandle.o \
  fmo.o \
  header.o \
  image.o \
  intra16x16_pred.o \
  intra16x16_pred_mbaff.o \
  intra16x16_pred_normal.o \
  intra4x4_pred.o \
  intra4x4_pred_mbaff.o \
  intra4x4_pred_normal.o \
  intra8x8_pred.o \
  intra8x8_pred_mbaff.o \
  intra8x8_pred_normal.o \
  intra_chroma_pred.o \
  intra_chroma_pred_mbaff.o \
  intra_pred_common.o \
  ldecod.o \
  leaky_bucket.o \
  loopFilter.o \
  loop_filter_mbaff.o \
  loop_filter_normal.o \
  macroblock.o \
  mbuffer.o \
  mbuffer_mvc.o \
  mb_access.o \
  mb_prediction.o \
  mb_read.o \
  mc_direct.o \
  mc_prediction.o \
  nal.o \
  nalu.o \
  output.o \
  parset.o \
  quant.o \
  read_comp_cabac.o \
  read_comp_cavlc.o \
  rtp.o \
  sei.o \
  transform8x8.o \
  vlc.o

.PHONY: clean
../bin/ldecod.exe:$(OBJS)
	@echo 'creating binary "$@"'
	@$(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
	@echo '... done'
clean:
	@echo remove all objects
	$(RM) $(OBJS:%.o=%.d) $(OBJS) ldecod.exe
-include $(OBJS:%.o=%.d)

おまけ

visual studio 用の build.ninja も作ってみました。 chocolatey で install される version 1.8.2 では、バグを踏んで動かないので注意ください。ビルドは 6.3088257秒でした。早い。 docker 立ち上げないからだとおもいます。

build.ninja
root = .
builddir = build_win
exedir = bin

msvc_deps_prefix = メモ: インクルード ファイル:
cflags = /Wall /Os
cppflags = /DNDEBUG /DWIN64 /I$root\lcommon\inc /I$root\ldecod\inc
ldlibs = Ws2_32.lib

rule compile
     description = compiling object file "$out" ...
     deps    = msvc
     command = cl /nologo /showIncludes $cflags $cppflags /c /Fo$out $in

rule link
     description = creating binary "$out"
     command = link /NOLOGO $ldflags $target_arch $loadlibes $ldlibs /OUT:$out $in

build $exedir\ldecod.exe : link $
  $builddir\blk_prediction.obj $
  $builddir\config_common.obj $
  $builddir\img_io.obj $
  $builddir\img_process.obj $
  $builddir\input.obj $
  $builddir\io_raw.obj $
  $builddir\io_tiff.obj $
  $builddir\mbuffer_common.obj $
  $builddir\memalloc.obj $
  $builddir\mv_prediction.obj $
  $builddir\nalucommon.obj $
  $builddir\parsetcommon.obj $
  $builddir\resize.obj $
  $builddir\transform.obj $
  $builddir\win32.obj $
  $
  $builddir\annexb.obj $
  $builddir\biaridecod.obj $
  $builddir\block.obj $
  $builddir\cabac.obj $
  $builddir\configfile.obj $
  $builddir\context_ini.obj $
  $builddir\decoder_test.obj $
  $builddir\dec_statistics.obj $
  $builddir\erc_api.obj $
  $builddir\erc_do_i.obj $
  $builddir\erc_do_p.obj $
  $builddir\errorconcealment.obj $
  $builddir\filehandle.obj $
  $builddir\fmo.obj $
  $builddir\header.obj $
  $builddir\image.obj $
  $builddir\intra16x16_pred.obj $
  $builddir\intra16x16_pred_mbaff.obj $
  $builddir\intra16x16_pred_normal.obj $
  $builddir\intra4x4_pred.obj $
  $builddir\intra4x4_pred_mbaff.obj $
  $builddir\intra4x4_pred_normal.obj $
  $builddir\intra8x8_pred.obj $
  $builddir\intra8x8_pred_mbaff.obj $
  $builddir\intra8x8_pred_normal.obj $
  $builddir\intra_chroma_pred.obj $
  $builddir\intra_chroma_pred_mbaff.obj $
  $builddir\intra_pred_common.obj $
  $builddir\ldecod.obj $
  $builddir\leaky_bucket.obj $
  $builddir\loopFilter.obj $
  $builddir\loop_filter_mbaff.obj $
  $builddir\loop_filter_normal.obj $
  $builddir\macroblock.obj $
  $builddir\mbuffer.obj $
  $builddir\mbuffer_mvc.obj $
  $builddir\mb_access.obj $
  $builddir\mb_prediction.obj $
  $builddir\mb_read.obj $
  $builddir\mc_direct.obj $
  $builddir\mc_prediction.obj $
  $builddir\nal.obj $
  $builddir\nalu.obj $
  $builddir\output.obj $
  $builddir\parset.obj $
  $builddir\quant.obj $
  $builddir\read_comp_cabac.obj $
  $builddir\read_comp_cavlc.obj $
  $builddir\rtp.obj $
  $builddir\sei.obj $
  $builddir\transform8x8.obj $
  $builddir\vlc.obj

build $builddir\blk_prediction.obj : compile $root\lcommon\src\blk_prediction.c
build $builddir\config_common.obj : compile $root\lcommon\src\config_common.c
build $builddir\img_io.obj : compile $root\lcommon\src\img_io.c
build $builddir\img_process.obj : compile $root\lcommon\src\img_process.c
build $builddir\input.obj : compile $root\lcommon\src\input.c
build $builddir\io_raw.obj : compile $root\lcommon\src\io_raw.c
build $builddir\io_tiff.obj : compile $root\lcommon\src\io_tiff.c
build $builddir\mbuffer_common.obj : compile $root\lcommon\src\mbuffer_common.c
build $builddir\memalloc.obj : compile $root\lcommon\src\memalloc.c
build $builddir\mv_prediction.obj : compile $root\lcommon\src\mv_prediction.c
build $builddir\nalucommon.obj : compile $root\lcommon\src\nalucommon.c
build $builddir\parsetcommon.obj : compile $root\lcommon\src\parsetcommon.c
build $builddir\resize.obj : compile $root\lcommon\src\resize.c
build $builddir\transform.obj : compile $root\lcommon\src\transform.c
build $builddir\win32.obj : compile $root\lcommon\src\win32.c            

build $builddir\annexb.obj : compile $root\ldecod\src\annexb.c
build $builddir\biaridecod.obj : compile $root\ldecod\src\biaridecod.c
build $builddir\block.obj : compile $root\ldecod\src\block.c
build $builddir\cabac.obj : compile $root\ldecod\src\cabac.c
build $builddir\configfile.obj : compile $root\ldecod\src\configfile.c
build $builddir\context_ini.obj : compile $root\ldecod\src\context_ini.c
build $builddir\decoder_test.obj : compile $root\ldecod\src\decoder_test.c
build $builddir\dec_statistics.obj : compile $root\ldecod\src\dec_statistics.c
build $builddir\erc_api.obj : compile $root\ldecod\src\erc_api.c
build $builddir\erc_do_i.obj : compile $root\ldecod\src\erc_do_i.c
build $builddir\erc_do_p.obj : compile $root\ldecod\src\erc_do_p.c
build $builddir\errorconcealment.obj : compile $root\ldecod\src\errorconcealment.c
build $builddir\filehandle.obj : compile $root\ldecod\src\filehandle.c
build $builddir\fmo.obj : compile $root\ldecod\src\fmo.c
build $builddir\header.obj : compile $root\ldecod\src\header.c
build $builddir\image.obj : compile $root\ldecod\src\image.c
build $builddir\intra16x16_pred.obj : compile $root\ldecod\src\intra16x16_pred.c
build $builddir\intra16x16_pred_mbaff.obj : compile $root\ldecod\src\intra16x16_pred_mbaff.c
build $builddir\intra16x16_pred_normal.obj : compile $root\ldecod\src\intra16x16_pred_normal.c
build $builddir\intra4x4_pred.obj : compile $root\ldecod\src\intra4x4_pred.c
build $builddir\intra4x4_pred_mbaff.obj : compile $root\ldecod\src\intra4x4_pred_mbaff.c
build $builddir\intra4x4_pred_normal.obj : compile $root\ldecod\src\intra4x4_pred_normal.c
build $builddir\intra8x8_pred.obj : compile $root\ldecod\src\intra8x8_pred.c
build $builddir\intra8x8_pred_mbaff.obj : compile $root\ldecod\src\intra8x8_pred_mbaff.c
build $builddir\intra8x8_pred_normal.obj : compile $root\ldecod\src\intra8x8_pred_normal.c
build $builddir\intra_chroma_pred.obj : compile $root\ldecod\src\intra_chroma_pred.c
build $builddir\intra_chroma_pred_mbaff.obj : compile $root\ldecod\src\intra_chroma_pred_mbaff.c
build $builddir\intra_pred_common.obj : compile $root\ldecod\src\intra_pred_common.c
build $builddir\ldecod.obj : compile $root\ldecod\src\ldecod.c
build $builddir\leaky_bucket.obj : compile $root\ldecod\src\leaky_bucket.c
build $builddir\loopFilter.obj : compile $root\ldecod\src\loopFilter.c
build $builddir\loop_filter_mbaff.obj : compile $root\ldecod\src\loop_filter_mbaff.c
build $builddir\loop_filter_normal.obj : compile $root\ldecod\src\loop_filter_normal.c
build $builddir\macroblock.obj : compile $root\ldecod\src\macroblock.c
build $builddir\mbuffer.obj : compile $root\ldecod\src\mbuffer.c
build $builddir\mbuffer_mvc.obj : compile $root\ldecod\src\mbuffer_mvc.c
build $builddir\mb_access.obj : compile $root\ldecod\src\mb_access.c
build $builddir\mb_prediction.obj : compile $root\ldecod\src\mb_prediction.c
build $builddir\mb_read.obj : compile $root\ldecod\src\mb_read.c
build $builddir\mc_direct.obj : compile $root\ldecod\src\mc_direct.c
build $builddir\mc_prediction.obj : compile $root\ldecod\src\mc_prediction.c
build $builddir\nal.obj : compile $root\ldecod\src\nal.c
build $builddir\nalu.obj : compile $root\ldecod\src\nalu.c
build $builddir\output.obj : compile $root\ldecod\src\output.c
build $builddir\parset.obj : compile $root\ldecod\src\parset.c
build $builddir\quant.obj : compile $root\ldecod\src\quant.c
build $builddir\read_comp_cabac.obj : compile $root\ldecod\src\read_comp_cabac.c
build $builddir\read_comp_cavlc.obj : compile $root\ldecod\src\read_comp_cavlc.c
build $builddir\rtp.obj : compile $root\ldecod\src\rtp.c
build $builddir\sei.obj : compile $root\ldecod\src\sei.c
build $builddir\transform8x8.obj : compile $root\ldecod\src\transform8x8.c
build $builddir\vlc.obj : compile $root\ldecod\src\vlc.c

感想

クリーンビルドでは差が無いのでは無いかと思っていたが、ninja が倍近く早くてびっくりしました。積極的に ninja を使う理由になります。
ただ、build.ninja は makefile に比べて記述量が増えるのは間違いないです。面倒です。
include文 を駆使することで、 makefile でいうところの wildcard 的な記述ができないか、実験してみたいと思います。まぁ、駄目な気もしますが。設計思想で wildcard は 速度低下の元になるので、あえてサポートしていないそうなんで。

参考リンク

MS-DOSコマンドで処理時間を計測する

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?