JUNのブログ

JUNのブログ

活動記録や技術メモ

HackDay 2021 Online に参加しました

2021年3月20日〜21日に開催された HackDay2021Online に参加しました. その振り返り記事です.

HackDayとは

一応知らない人のためにHackDayとはなんぞやというのを簡単に説明すると, Yahooが主催する「24時間で開発を行うハッカソン」です. 24時間で開発とプレゼン資料とかもろもろ作って, そんで発表するという感じです.

作ったもの

今回私のチームは BrailleBeat という点字音ゲーを組み合わせたゲームをUnityで制作しました.

発表は以下のアーカイブで見れます.

youtu.be

開発時点ではこのゲームはiPadアプリとして作っていたのですが, 開発がある程度完了した時点で時間に余裕があったのでWebGL版も作って公開しました.

junnetworks.github.io

結果

この記事を書いている時点(2021/03/23)ではまだHappyHacking賞(投票によって決まる賞)以外の最優秀賞や企業賞などは決まっていないです.

ちなみに視聴者投票の結果は以下のようになりました.

f:id:JUN_NETWORKS:20210323192609p:plain

なんと!! 私達のチームの作品が2位です!!

感想

今回久しぶりにハッカソンに出ましたが, めちゃくちゃ楽しかったです!!

また, 視聴者投票の結果2位になれたことは大変嬉しい結果でした. ただ, 自分は今回の作品に自信があったので, 2位という結果が嬉しいと同時に少し悔しいです.

開発に関して

開発に関しては今回自分たちは完全オンラインで行いました. チームメンバーは以下のような分担で開発を行いました.

  • Diceさん: ゲームのコア部分とタップエフェクトの実装
  • みさきくん: スタート画面やオーディオビジュアライザーの実装
  • フレキくん: 譜面生成とプレゼン資料作成&発表
  • みずのさん: 全てのデザイン
  • JUN(俺): リザルト画面やWebGLビルド

上記に書いた以外にも色々とみんなやったりしましたが, 大体こんな感じの分担でした.

皆さんとても優秀でとてもスムーズに開発を行うことができ, 結果として完成度の高い作品が開発できました.

今回お互いが皆顔見知りというわけではなく, 私の場合だとDiceさんとみずのさんとは今回のハッカソンで初めましてという感じでしたが, それが逆に新鮮でとても楽しかったです. 新しい人との繋がりはいつでも良いものですね.

開発のスケジュール感としては最終的に以下のような感じになりました. (Discordのログとコミットログを参考に大体こんな感じ)

  • 20日 12:00: 開発開始
  • 20日 13:00: 譜面ファイルの形式を決定
  • 20日 18:00: ゲームとしてコア部分(判定やノーツの配置など)がある程度完成
  • 21日 00:00: 演奏画面のデザインと実装, ひらがな表示
  • 21日 01:00: スコア計算などを実装
  • 21日 05:00: タップエフェクトなどを作成
  • 21日 10:00: WebGL版デプロイ完了

github.com

チームメンバーの皆さんのおかげで余裕を持った開発が出来たと感じています.

イデアについて

今回は"何を作るか"ということに関してはHackDayが始まる前にDiscordのボイチャにて2回ほどアイデア出しを行い決めました.

イデア出しは「エンジニアの知的生産性」で学んだKJ法を取り入れてみました.

jun-networks.hatenablog.com

KJ法は本来オフラインで付箋を使って行うのですが, 今回は完全オンラインなので代わりにMiroというコラボレーションツールを使ってKJ法行いました.

miro.com

f:id:JUN_NETWORKS:20210323194046p:plain
Miroを使ったアイデア出し

まず, 各々が思いついたワードやそれに関連するワードやそのワードに対する感情などを15~30分くらいで書きだしてもらい, その後各々が書いたワードについて書いた人がなぜそのワードを書いたかを説明し, それに対してフィードバックを行い, これを全ての人が行います. その後関連するワード, 今回の場合だと"コロナ関連", "食事系" などのように関連性の高そうなワードを集めて大まかにカテゴライズします. そしたらそこから思いついた具体的なアイデアを書き出してもらい, そのアイデアの中から決めるというような流れでKJ法をやってみました.

f:id:JUN_NETWORKS:20210323194618p:plain
私が書いたワード

ちなみに今回の 点字×音ゲー の案はDiceさんが昔思いついたものでしたので, KJ法によって生み出されたアイデアかと問われたら少し違うかもしれませんが, それでも多くの作品のアイデアが生まれたのでやって良かったと個人的には思っています.

f:id:JUN_NETWORKS:20210323195702p:plain
BrailleBeatの付箋たち (この時点では名前が決まっていなかった)

f:id:JUN_NETWORKS:20210323203153p:plain
採用されたデザイン案

完走した感想

オンラインハッカソン, オフラインハッカソンと同じように楽しめたと思いました.

ただ, 他のチームとの交流や新たな出会いなどがやはりオフラインに比べて少ないと感じました. これはオンラインハッカソンに限らず, オンラインで開催されるイベントは概ねこの傾向が強いので仕方ないといえばそうですが... この辺なんとかテクノロジーの力で解消出来ないものか...

あと, 今回初めてデザイナーの方と一緒にハッカソンに出たのですが, デザイナーって凄いですね. この凄さを上手く言語化するのはなかなか難しいのですが, なんというかエンジニアが作った建物を綺麗にインテリアや壁紙などを整えるというか, なんかデザイナーさんがデザインしてくれるとなんか作品が華やかになるんですよね... 自分がデザインすると頑張ったところで2000年代個人サイトくらいが限度なので本当に凄いです.

今回多分1年以上ぶりにハッカソンに出たんですが, なんというか プログラミングの楽しさ というか ものづくりの楽しさ というのを思い出した気がします.

今回, HappyHacking賞を惜しくも逃したわけですが, 他にも賞はあるのでちょっぴり期待して連絡を待とうと思います.

追記: 他の賞は特に何もありませんでした.

というわけで久しぶりのハッカソン楽しかったゾイというお話でした. ではまた次の記事でお会いしましょう. ではでは〜〜👋👋

(ちなみにチーム名の由来はこんな感じです)

f:id:JUN_NETWORKS:20210323205104p:plain
チーム名が「なんでもいいですよ」に決まった瞬間

「Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識」を読んだ

「[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識」を読んだ.

Linuxを日頃から触っているけど, 中身を全然知らなかったので読んだ.

Linuxの内部の仕組みを大まかに理解出来るよい本だった.

本書は実験プログラムと豊富な図解と丁寧な解説で自分のようなOSやコンピューターアーキテクチャについて殆ど知識が無いような人でも詰まる事無く理解出来た. (Linux触ったこと無い人には難しいかもしれん)

この本を読めばLinuxカーネルを完全に理解したとはならない気がする. どちらかというと, これからLinuxカーネルやOSをがっつり勉強していくという人の初めの第一歩に最適だと思った. (初手ヘネパタ本とか挫折しそうだし)

後はLinuxを日頃から使っているエンジニアなんかも読んでおくと日頃から触っているシステムについて詳しく慣れるので良いと思った.

手を動かしながら楽しく読める本だったのでおすすめです.

第1章 コンピュータシステムの概要

この章ではOSがコンピューターにおいてどのような役割を担っており, それによって何が嬉しいのかが書かれている.

OSが無いと個々のプログラムが各ハードウェアデバイスにアクセスするためにハードウェアデバイスごとにアセンブリなどでコード書かないといけないの大変だよねー. じゃあそれをまとめて共通のインターフェースとして使えるようにすれば楽じゃん. って感じか.

このあたりの内容は前から知っているものだったので, まぁ確かにそうだな って感じ.

第2章 ユーザーモードで実現する機能

ハードウェアの抽象化などの役割がOSにあるのは前から知っていたが, この章ではCPUのユーザーモードカーネルモードというのが出てきてこれは初めて知ったのでびっくりした.

自分はC言語でwrite, readなどのシステムコールを何度も使ったことがあったが, 実際のところそれらがどのように動いているのかは何も知らなかった. しかし, ここではCPUのモードという概念が登場し, システムコールが呼ばれた際にはCPUがユーザーモードからカーネルモードに移動して, システムコールを処理することがわかった.

システムコールにはプロセスの生成やメモリの確保,解法,プロセス間通信,ネットワーク, ファイルシステム操作, デバイスアクセスなどの色々な種類があり, それらの機能を使いたい時にシステムコールという形でカーネルに依頼する.

システムコールはCPUの特殊な命令を実行することで発行するらしい. プロセスは通常ユーザーモードで動作するが, システムコールが発行されるとCPUはカーネルモードに遷移して, 依頼内容に応じたカーネルの処理が動き, それが完了したらまたユーザーモードに戻る.

そしてユーザープロセスがシステムコールを使わず直接CPUのモードを変更するは無い. (もしあるならカーネルの意味がない)

第3章 プロセス管理

ここではカーネルによるプロセス生成,削除,管理などについて説明してる.

Linuxには fork()execve() という2つの関数がある.

  • fork(): 同じプログラムの処理を複数のプロセスに分けて処理する という目的で使う(Webサーバーによる複数リクエストの受付). 処理としては,
    1. 子プロセス用にメモリ領域を確保し, 親プロセスのメモリをコピーする.
    2. fork()関数の実行が完了したところからプログラムは再開する. (親プロセスのメモリを丸々コピーしているのでプログラムカウンタもコピーされてるって感じか?)
    3. 親プロセスと子プロセスは一般的に違うコードを実行する. これには fork() 関数の戻り値が親プロセスでは子プロセスのプロセスID, 子プロセスでは0が返ってくることを利用して分岐させる.
  • execve(): 全く別のプログラムを生成する時に使う(bashから各種プログラムの新規作成). 処理としては,
    1. 実行ファイルからプロセスに必要なメモリマップを読み出す.
    2. 現在のプロセスのメモリを新しいプロセスのデータで上書きする.
    3. 新しいプロセスの最初の命令から実行を開始する.

ちなみにメモリマップとは, 「データやコードがメモリ内にどのように配置されるか?」という情報のこと. (で合ってるはず...)

どの命令(のアドレス)から実行すればよいかは実行ファイル内に書かれている.

Linuxにおいては実行ファイルはELF(Executable Linkable Format)という形式になっており, プロセスに必要なメモリマップはELFファイルのプログラムヘッダの部分の情報を元に計算されるのかな?

ja.wikipedia.org

別のプロセスを新規作成する場合は, 親となるプロセスが fork() を読んで, 生成された子プロセスがexecve()を呼ぶ "fork and exec" によるプロセス生成が多いらしい.

github.com

あと, 勝手に勘違いしていたんだけど, 親プロセスが死んでも(終了しても)子プロセスが芋づる式に死ぬわけでは無いっぽい. そもそも親プロセスという言い方をしているが, 正確には 「親となるプロセス」であり, 親となるプロセスからfork()によって生成されたプロセスは別に親のことなんかどうでもいいっぽい.

親プロセスが死んだら, 子プロセスは initプロセス(initはsystemdのシンボリックリンク)の養子になるらしい.

子プロセスを残したまま親プロセスがkillされてしまった場合その子プロセスはどうなるのだろうか?

その場合Linuxでは「initプロセスの養子」となるようになっている。「親プロセスがいなくなったからといって子プロセスが停止するとは限らない」ところに気をつけてほしい。

しかし一般的なデーモンプログラムなどは自分が死ぬときは基本的に子プロセスをkillしてから寿命をまっとうするようになっている場合が多いだろう。

eng-entrance.com

自分はNginxのプロセスとかをイメージして勝手に親プロセスが死んだら子プロセスも死ぬと思っていたけど, これはただ単に多くのプログラムがそのように実装しているだけらしい.

プロセスの親プロセスをpsコマンドで見る方法は以下のリンクを参照.

www.atmarkit.co.jp

第4章 プロセススケジューラ

この章ではプロセススケジューラについて説明してる.

プロセススケジューラによってプロセスは一定時間(タイムスライス)ごとにプロセスが切り替わりながら実行される.

また1つのコアに付き同時に実行できるプロセスは1つなので(ハイパースレッドは無視してもろて...), 昨今のCPUのコア数増加のありがたさがわかった.

で, この章では論理CPU(OSが認識出来るCPUの数)とプロセスを変えると実行時間がどのようになるかという実験をした.

結論としては,

  • プロセス数とコア数が同じ: 1つのプロセスが1つのコアを独占して使えるので最速.
  • プロセス数がコア数より多い時: 1つのコアで複数プロセスを切り替えながら実行する(これをコンテキストスイッチと呼ぶ)ので, 1つのプロセスが1つのコアの時に比べて遅くなる. 単純に, 4プロセスを2コアで動かすと2プロセスを2コアで動かしたときよりも1プロセスにかかる時間が2倍になる.

ちなみにコンテキストスイッチはプロセスの切りの良いところで発生するものではなく, タイムスライスが切れるとそのプロセスが今何を実行していようが容赦なく次の別のプロセスの実行へと切り替わる.

プロセスには状態があり, 状態には以下のようなものがある.

  • 実行状態: 現在論理CPUを使っている
  • 実行待ち状態: CPU時間が割り当てられるのを待っている
  • スリープ状態: 何らかのイベントの発生を待っている. イベント発生まではCPU時間を使わない. イベントが発生したら実行待ち状態に移行する.
  • ゾンビ状態: プロセスが終了した後に親プロセスが終了状態を受け取るのを待っている.

とりあえずこの4つを覚えればいいっぽい.

イベントっていうのは, 所定時間(3秒とか)が経過するの待っていたり, キーボードなどのデバイスの入力待ちだったり, ストレージデバイスI/O待ちだったり, ネットワークの送受信待ちだったりいろいろある.

各プロセスの状態は ps ax のSTATフィールドを見ればわかる.

qiita.com

論理CPUが複数ある場合のプロセススケジューリングはプロセススケジューラの中にあるロードバランサによって複数の論理CPU間で公平にプロセスを分配するようになっている.

例えば論理CPUが2つあって, CPU0にはプロセスが2つあり, CPU1にはプロセスが1つある場合, ここで新しいプロセスを生成したら, そのプロセスはCPU1のほうで実行されることになる.

timeコマンドによって表示される各項目の意味はいかのようなものである.

real 11.12  # 実際の経過時間
user 11.09  # CPUがユーザーモードで動作した時間
sys 0.00    # システムコールを実行していた時間(CPUがカーネルモードで動作した時間)

ちなみにtimeコマンドの user と sys の部分は, 2つの論理CPUで動かした場合は2つのCPUで動作した時間を合計したものになるので, real の項目より user や sys の項目の値のが方が大きくなることがある.

プロセスには優先度(nice値)というものがあり, nice() というシステムコールによって-19から20までの間で変更が可能である. デフォルトは0. -19が一番優先度が高く, 20が一番優先度が低い. 優先度が高いプロセスほどCPU使用時間の割合が他のプロセスに比べて長くなる.

優先度を上げるのはroot権限が必要だが, 優先度を下げるのは誰でも出来る.

第5章 メモリ管理

Linuxは, システムに搭載されている全メモリを, カーネルのメモリ管理システムと呼ばれる機能を使って管理している.

で, 普通にプログラムが直にメモリを触れてしまうと, メモリの確保,解放の積み重ねでメモリの断片化が起きたり, あるプロセスが別のプロセスのメモリ領域にアクセス出来たり, メモリ配置がハードウェアアドレスの番地なのでマルチプロセスの際にいちいち他のプログラムに配慮しなくてはいけなかったりと大変なので, 仮想記憶という仕組みを使う.

仮想記憶とは, カーネルが使うメモリ内にページテーブルを保持し, そのページテーブルに仮想アドレスと物理アドレスの変換を保存しておき, プロセスがメモリにアクセスする際にはそのページテーブルを元に物理アドレスにアクセスするという感じ.

ページテーブルはメモリをページという単位で区切って管理していて, 変換はページ単位で行われる. ページテーブルの中の1つのページに対応するデータをページエントリと呼ぶ. ページサイズはCPUアーキテクチャごとに決まっていて, x86_64の時は4Kバイト.

仮想記憶(ページテーブル)のおかげで, メモリ断片化の問題は, 空きメモリを上手いこと仮想アドレスに紐付ければOK. あるプロセスが別のメモリ領域にアクセス出来る問題は, ページテーブルの仮想アドレスの範囲外にアクセスしようとしたらページフォールトというCPU割り込みが発生し, カーネルがそれを捉え, SIGSEGVというシグナルをプロセスに通知し, プロセスを強制終了させる. 仮想アドレスを使えば各プロセスは自分の好きなようにアドレスを指定出来る(物理アドレスを考えなくてもよい)ので他のプロセスと共存することが可能.

で, 実際のLinuxカーネルはページテーブルを使うだけではなく, デマンドページング と呼ばれる方法を使ってメモリを管理している.

この方法は, メモリ確保のシステムコールが呼ばれた際にはページテーブルに物理アドレスが未定のページエントリを作成し(仮想メモリの確保), その仮想アドレスに初めてアクセスが来た時に初めて実際にその仮想アドレスに物理アドレスを紐付ける(物理メモリの確保)というもの.

この流れとしては,

  1. プログラムが仮想メモリは割当済みだが物理メモリ未割り当ての領域にアクセス
  2. CPUがアクセスしようと試みるが, 未割り当てなのでページフォールトが発生.
  3. カーネルページフォールトを検知し物理メモリを仮想メモリに割り当てる
  4. ユーザーモードに戻ってプロセスの実行を継続する.

なので, 例えば8GBのメモリ搭載のシステムで100GBのメモリを確保(仮想メモリの確保)は可能であるが, 実際にアクセスしようとした時にエラーになる.

malloc() は実はメモリを毎回確保する処理をしているわけではなく, ある程度mmap()でメモリを確保し, glibcのメモリプールとして確保したメモリ(仮想メモリ)を保持しておき, malloc() が呼ばれた際にバイト単位で切り出してプログラムにかえしている. ちなみにメモリプールが足りなくなるとまた mmap() でメモリを確保する. ちなみに mmap() はページ単位でメモリを獲得するのでそのままだとバイト単位でもメモリ確保は出来ない.

実は fork() も仮想記憶によって高速化されている. fork() 実行時はメモリをまるごとコピーするのではなく, 親プロセスのページテーブルのみコピーしてくる. そして, 親か子プロセスのどちらか変更を加えたらそのページのコピーが作られ, 書き込み側のプロセスのページテーブルの仮想アドレスに対応する物理アドレスが更新され, メモリの共有が解除される. メモリが共有されたらもう一方のプロセスも読み書きを自由に出来るようなる. この仕組みをコピーオンライトという.

メモリが足りねぇーってなったら, 一部メモリ内のデータをスワップメモリに退避させてメモリに空きを作る. スワップメモリとはストレージデバイスを一時的にメモリとして扱うものである. この時退避先の領域のことをスワップメモリという. メモリからスワップ領域に退避させることをスワップアウトという. 退避させたメモリのページエントリの物理メモリにはスワップ領域のアドレスが書かれている.

逆にスワップ領域に退避させていたデータにアクセスしようとした場合には, スワップ領域からメモリ領域に戻す処理(スワップイン)が行われる.

スワップアウト, スワップイン合わせてスワッピングという.

スワップ領域は便利なものだが, ストレージデバイスはメモリに比べて格段に遅いので, スワッピングが多発する場合にはシステムの速度が低下することがある. このようなスワッピングが繰り返されてシステムの速度が低下することをスラッシングと呼ぶ.

ページテーブル自体が肥大化しないように階層型ページテーブルやヒューページとかいう仕組みを使ってうまいことしているらしい.

第6章 記憶階層

CPUからレジスタへのアクセス時間に比べてメモリへのアクセス時間は極端に遅い.

なので, CPU内にはキャッシュメモリと呼ばれるメモリから読み出した情報を貯めておく記憶領域があり, メモリへのアクセスを減らす工夫がなされている. キャッシュメモリはメモリアクセスに比べて数倍から数十倍早い.

一度目の特定のメモリへのアクセスはメモリからキャッシュメモリに読み出して, そんでキャッシュメモリから読み出す. 2回目以降はキャッシュメモリからの読み出しのみで済むので高速になる.

キャッシュメモリのデータを変更した場合には即時にメモリに変更が反映されるのではなく, バックグラウンド処理としてメモリに書き戻される.

キャッシュメモリが足りなくなったら既存のキャッシュライン(キャッシュの1単位)を破棄する. その時, ダーティフラグが立っていたらメモリに書き戻してから破棄する.

キャッシュメモリは階層構造になっていてL1,2,3キャッシュというのがある.

で, キャッシュメモリ内にデータがあれば高速化するけど, 実際に上手く働かくかというのは, 多くの場合YES. 多くのプログラムは参照の局所性と呼ばれる次のような特徴がある.

  • 時間的局所性: ある時点でアクセスされたデータは近い将来再びアクセスする可能性が高い. (ループの処理のコード領域とか)
  • 空間的局所性: ある時点であるデータにアクセスされると, それに近い場所にアクセス可能性が高い. (配列要素の全走査など)

あと, 今のままだとページテーブルの参照が結局物理メモリにアクセスしていて遅いので, Translation Lookaside Bufferというページテーブルをキャッシュメモリと同様に高速アクセス可能な領域があるらしい.

ja.wikipedia.org

で, ストレージデバイスへのアクセスはメモリに比べてもさらに激遅なので, メモリキャッシュのストレージ版であるページキャッシュという機能がある.

これはファイルからデータを読み出す時に, 直接データをプロセスのメモリ空間内にコピーするのではなく, 一度カーネル用メモリ内にページキャッシュとして保存し, それをプロセスのメモリ空間内にコピーするというものである.

書き込みについても同様で, 直接ストレージデバイスを書き換えるのではなく, ページキャッシュを書き換え, 書き換えられた箇所はバックグラウンドでストレージデバイスに反映されるようになっている.

ちなみに, ページキャッシュはカーネル内のメモリにあるので, 別のプロセスが同じファイルを読み出そうとした場合にも効果を発揮する.

ページキャッシュにもダーティフラグ的なものがあり, 考え方はキャッシュメモリのものとほぼ同じ.

ちなみにダーティフラグが立ったページが存在した状態(ストレージデバイスにまだ反映されていないページがある状態)で電源を消すとその書き換わったページの部分のデータは消滅する.

消滅してほしくなかったら同期的な書き込みを有効にするフラグ O_SYNC フラグを open() 時に指定して上げれば良い. こうすると書き込んだ時にページキャッシュだけでなくストレージデバイスにも同時に書き込まれる.

ここまで述べた通り, CPUは常に計算している訳ではなく, CPUの計算速度に比べてメモリアクセスやストレージデバイスはとても時間がかかるのでCPUには割と暇な時間(データ転送待ち)がある. そこで, ハードウェア側で認識できる論理CPUの数を2倍にして, 一定の条件下で2つのCPUがあるかのように動作させるハイパースレッドという機能がある. ただし, 必ずしも性能が向上するわけではなく, 性能が向上したりしなかったりするので, そのへんは注意が必要.

7章 ファイルシステム

ストレージデバイスを直接扱うということは各ファイルがどのメモリ番地から何バイトにあたって存在するかなどを記録しておかねばならないのでめんどくさすぎる.

なのでファイルシステムというものを使う.

ファイルシステムには ext4, XFS, Brtfs など色々種類があるが全て統一的なインターフェースでアクセス出来るようになっており, ユーザープログラム側はファイルシステムの差異を気にしなくても良いように出来ている.

それぞれのファイルシステムの違いとしてはファイルシステムの不整合時の修復方法の違いなど色々あるっぽいので用途に合わせて選ぶのが良さそう. Linuxだとext4がデフォルトっぽい?

ファイルには2つのデータがある

  • データ: 普通にデータ. 動画とかテキストとか写真とか.
  • メタデータ: ファイルの名前やストレージデバイス上での位置, サイズなどの補助的な情報. 情報としては以下のようなものがある.
    • 種類: ファイルかディレクトリかその他か
    • 時刻情報: 作成した時間, 最後にアクセスした時間, 最後に内容を変更した時間
    • 権限情報: どのユーザーがファイルにアクセス出来るか.

で, こっからおもしろくて, ファイルとディレクトリ以外にもデバイスファイルというものがある. Linuxは自分が動作しているハードウェア上のデバイスをほぼ全てファイルとして表現している. (Everything is a file)

unix.stackexchange.com

各デバイス/dev 以下に存在する.

バイスファイルには2種類ある

  • キャラクタデバイス: 端末, キーボード, マウスなどの読み書きは出来るがシークは出来ないもの.
  • ブロックデバイス: HDDやSSDなどのストレージデバイス.

例えば端末のデバイスファイルは wirte() システムコールでデータを端末に出力でき, read()システムコールでデータを端末から読み取れる.

$ ps
    PID TTY          TIME CMD
2873558 pts/10   00:00:00 ps
4186478 pts/10   00:00:03 zsh
$ echo "HELLO" > /dev/pts/10 #別端末から実行すると pts/10 に対応する端末に文字が出力される

qiita.com

そして実はファイルシステムを介さずに直接ストレージデバイスの対応する番地のデータを書き換えることも出来る. ただし, これはあくまで実験用であって, 普通に使うとシステムが壊れる可能性があるのでしないほうが良い.

ファイルシステムには他にメモリベースのtmpfsという /tmp/var/run などの再起動したら消えるディレクトリで使わるものもある.

$ mount | grep tmpfs
udev on /dev type devtmpfs (rw,nosuid,noexec,relatime,size=8048832k,nr_inodes=2012208,mode=755)
tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,relatime,size=1615600k,mode=755)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
tmpfs on /run/snapd/ns type tmpfs (rw,nosuid,nodev,noexec,relatime,size=1615600k,mode=755)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=1615596k,mode=700,uid=1000,gid=1000)

他にもネットワークデバイス用とかプロセス管理用(/procで使われる)とか色々種類がある.

第8章 ストレージデバイス

ここではHDDやSSDがセクタという単位で読み出している的な話がされていた.

で, HDDはアクセスの際に機械的な動作であるスイングアームの調整とかでめっちゃ時間がかかると.

なので機械的な動作をなるべく減らすために, 連続したセクタを一度に読み出すようにした方が早い. なので, カーネル側のブロックデバイス層という部分のI/Oスケジューラという機能が連速したセクタにアクセスする場合には一度にまとめて取得するとか読み出す順番をソートするとか色々最適化してくれている.

また, 先読みという機能(これはプログラムの局所性に基づくものである)もある.

以下読んでる時に書いた雑なメモ

p7
プロセスが直接デバイスに触れないようにするためにハードウェアレベルで制限を行う.
具体的にはCPUには カーネルモードユーザーモードがあり, カーネルモードの時のみデバイスへアクセス出来る.
プロセスはユーザーモードで動作し, カーネル(デバイスドライバやメモリ管理システムなどのプロセスに触られたくないもの)はカーネルモードで動作する.
プロセスがデバイスアクセスなどのカーネルモードで管理している部分を利用したいときはシステムコールを使ってOSに依頼する.
システムコールカーネルとプロセスとのインターフェース.

キーワード
- POSIX: UNIX系OSがが備えるべき各種機能を定めた規格

p25
OSが提供するプログラム例の中に ウィンドウシステム: X がある!

プログラム実行時に作成されたメモリマップは /proc/{pid}/maps というファイルによって得られる.

p110
- mmap() という関数でLinuxカーネルに対して新規にメモリを要求するシステムコールを呼ぶ. - mmap() はページ単位(通常は4KB)でメモリ領域を獲得する. - malloc() は内部的にmmap()を呼び出している. - malloc() のバイト単位でのメモリ確保を実現するために, 事前にmmap()システムコールによってカーネルから大きなメモリ領域を確保してプールして貯めておき, プログラムからのmalloc()発行時にその領域から必要な量をバイト単位で切り出してプログラムに返すという処理をしている. - プールしているメモリ領域がなくなればサイドmmap()を発行して新たなメモリ領域を獲得する.

p122: デマンドページング
- やはりmalloc()mmap() でメモリを獲得しても, 実際に物理メモリが確保されるのはそのメモリアドレスにアクセスした瞬間らしい. - 原理としては, mmap() などでメモリを確保する. (正確には 仮想メモリを確保する と言うらしい) - ページテーブル(仮想アドレスと物理アドレスを紐付けるやつ)にエントリを追加. ただしこの時点では物理アドレスの方は決まっていない. - プログラムが確保した領域にアクセスするとCPUがページフォールトを起こす. それをカーネルが検知して, 物理メモリを確保し, ページテーブルを書き換えて, プログラムの処理を続行する. (これを 物理メモリを確保した と表現する) - この時プログラムはページフォールトが発生したことに気づかない. - ちなみに2回目以降のアクセス時にはページフォールトは発生しない. なぜなら物理メモリが既に確保済みだから.

p132: メモリの枯渇
- メモリの枯渇には2種類ある - 仮想メモリの枯渇 - 仮想アドレス空間がいっぱいの状態でメモリを確保しようとすると失敗する. - これは32bit環境ではアドレス空間(ページングテーブル)のサイズが約4GBしか無かったので昔は大規模ソフトウェアでこの問題が発生していた. - いまは64bit環境で, 128TBくらいのアドレス空間があるので大体大丈夫. - 物理メモリの枯渇 - 物理メモリに空きが無かったら発生する.

スワッピング
- 物理メモリが足りなくなった際に, 一部メモリのデータをスワップ領域に移動させることをスワップアウトという. - スワップ領域に退避させていたデータにプログラムがアクセスした際にはページフォールトが発生し, スワップ領域から物理メモリにデータが移動される. これをスワップインという. - この2つのことを含めてスワッピングと呼ぶ. - スワッピングはページ単位で行われるので, ページングとも呼ばれたりする. その際はページアウト, ページインとなる.

p148 2種類のページフォールト
- メジャーフォールト: スワッピングのようにストレージデバイスへのアクセスが発生するページフォールトのこと - マイナーフォールト: ストレージデバイスへのアクセスが発生しないページフォールトのこと

p229: procfs : システムに存在するプロセスに関する情報を得るためのファイルシステム
- cat /proc/{pid}/maps : プロセスのメモリマップ - cat /proc/{pid}/cmdline : プロセスのコマンドライン引数 - cat /proc/{pid}/stat: プロセスの状態やこれまでに使用したCPU時間, 優先度, 使用メモリ量など - procfsではプロセス以外にも以下のような情報も得られる - /proc/info: システムに搭載しているCPUに関する情報 - /proc/diskstats: システムに搭載しているストレージデバイスに関する情報 - /proc/meminfo: システムに搭載しているメモリに関する情報 - /proc/sys/: カーネルの各種チューニングパラメータに対応するファイルが入っている. sysctl コマンドと/etc/sysctl.conf によって変更するパラメータに1対1で対応 - ps, sar, top, freeなどのOSがが提供する各種情報を表示するコマンドは procfs から情報を取得している. - 各種ファイルやディレクトリの詳細は man proc を参照.

p230: cgroup: 各種リソースの使用量に制限を設けるcgroupという機能があり, cgroupfsを通して操作する.
- CPUやメモリの使用量を制限することが出来る. - この機能はDockerなどのコンテナ管理ソフトウェアや, 複数の仮想マシンが動作するシステムで使われている. - 特に1つのシステム上に複数の顧客のコンテナや仮想マシンが共存するマルチテナント構成のシステムにおいて使われている.

使ったコマンド
- strace: strace ./a.out 呼ばれたシステムコールを表示する. - sar: sar -P ALL 1 1 とすると1秒間のCPUがどのモードになっているか割合が見れる. - sarコマンド自体はCPUやメモリ, ネットワーク, ディスクIOなどの負荷が見れるコマンド - ldd: ldd /bin/echo プログラムがどのようなライブラリをリンクしているかを見れる. - readelf: readelf -h /bin/sleep #エントリポイントアドレスの取得 Linuxの実行ファイルのフォーマットであるELF(Executable Linkable Format)の情報を見る. - readelf -S /bin/sleep # コードとデータのファイル内オフセット, サイズ, 開始アドレスを取得 - taskset: taskset -c 0 ./a.out コマンドライン引数で指定したプログラムを-cオプションで指定した論理CPU上で動作させる - ps ax でプロセスの状態を見る psコマンドまとめ - /proc/cpuinfo にCPUの情報が載っている - ps -eo pid,comm,time,etime: プロセスID, コマンド名, CPU使用時間, 経過時間 を表示する - free システムが搭載するメモリの量と使用中のメモリの量を見る. (出力はkB単位) - sar -r 1 でシステムの状態を1秒毎に表示 - sar -B 1でページングに関する情報を1秒毎に表示 - swapon --show システムのスワップ領域を表示 (free でも確認可能) - sar -W 1 でスワッピング(ページング)に関する情報を1秒毎に表示

関数
- system()C言語からシェルコマンド実行出来る (man 3 system)

C言語で3Dゲームを作った

42の課題の1つであるcub3Dを今日クリアしたので感想を書く.

課題について

Lv2 ではCG系の課題があり, cub3Dというレイキャスティングで古典的な3Dを作るか, miniRTというレイトレーシングプログラムを作るかを選ぶのですが, 自分はcub3Dの方を選びました. なぜなら画面動かせた方が楽しそうだったから.

レイキャスティングとはなんぞやって感じだけど, 大雑把に言えば「光をシミュレートして3次元空間を2次元空間に落とし込む」という感じかな? レイキャスティングのコアとなる考え方は, 光源から放たれる全ての光(光線)をシミュレートするのではなく, 逆に目(スクリーン)から光線(のようなにか)を発射してそれを計算することで何を映すかを計算する手法って感じ?

ちょっと言語化が下手くそ過ぎたので以下の動画の冒頭部分にレイキャスティングのわかりやすい概念の説明があるのでそれ見たほうがいいかも.

www.youtube.com

cub3Dがどんな課題かというと, 完成形としては Wolfenstein 3D みたいな3DゲームをC言語で作りましょうという課題.

www.youtube.com

やらなきゃいけないこととしては以下のような感じ

  • ゲームの設定ファイル(.cubファイル)から設定情報などをパースしてそれらの情報を元にゲームを遊べるようにする.
  • レイキャスティングを用いて3Dのような表示をする
  • --save フラグが引数として提供されたら.bmpファイルを出力してプログラムを終了する

って感じ. メインはレイキャスティングのところ.

使用が許可されている関数などは決まっていて, なんでもかんでも使えるわけではない. 一応 <math.h> 内の関数は全て使用しても良いということだったので, 三角関数などは実装しなくても良い.

また, 描画に関しては, 42側からX Window Systemのラッパーライブラリが提供されるのでそれを使う必要がある.

github.com

42では課題は与えられるけど, 授業や教科書は無いので自分は以下のサイトを見ながら作った.

lodev.org

作ったもの

コード

github.com

動いている様子

youtu.be

Wolfenstein 3D のテクスチャを使うとそれっぽくなります.

youtu.be

出力されたBMPをイメージビュワーで開いた様子. (なぜかはてなブログbmpファイルをうpするとバグったためスクショ)

f:id:JUN_NETWORKS:20210303035915p:plain

仕組み

norm_leave_comment ブランチにコメント付きのコードがあります. (ブランチ名ひどいな...)

github.com

壁の描画

github.com

プレイヤーの座標などの表し方

f:id:JUN_NETWORKS:20210304084150p:plain

プレイヤーの現在地は2次元ベクトル  \vec{pos} で表されます. またプレイヤーの現在向いている方向は2次元方向ベクトル  \vec{dir} で表され, これは大きさ1の単位ベクトルです.

ここからがポイントで, ここにスクリーンを表すカメラ平面ベクトル  \vec{plane} を追加します.  -\vec{plane} が画面左側,  +\vec{plane} が画面右側を表します. またこの  \vec{plane} の長さ(大きさ)によってプレイヤーの視野角が変わります.

視野角30° (  |\vec{plane}| \fallingdotseq 0.2679 )

f:id:JUN_NETWORKS:20210304085825p:plain
視野角30°

視野角66° (  |\vec{plane}| \fallingdotseq 0.6494 )

f:id:JUN_NETWORKS:20210304085928p:plain
視野角66°

視野角90° (  |\vec{plane}| \fallingdotseq 1.0000 )

f:id:JUN_NETWORKS:20210304090021p:plain
視野角90°

planeベクトルの長さの計算方法は  tan(deg/2) で計算出来ます.

また, マップのデータは文字列の配列で持ちます. (0: 空白, 1: 壁, 2: スプライト(障害物))

char *MAP[] = {
        "1111111111111111111111111",
        "1000000000110000000000001",
        "1011000001110000002000001",
        "100100000000000000000000111111111",
        "111111111011000001110000000000001",
        "100000000011000001110111110111111",
        "11110111111111011100000010001",
        "11110111111111011101010010001",
        "11000000110101011100000010001",
        "10002000000000001100000010001",
        "10000000000000001101010010001",
        "11000001110101011101011010N0111",
        "11110111 1110101 101111010001",
        "11111111 1111111 111111111111",
};

光線を飛ばす

以下の図のような感じでプレイヤーの現在地から  \vec{dir} - \vec{plane} から  \vec{dir} + \vec{plane} までの間で光線を飛ばします. 飛ばす間隔はスクリーンの横幅のピクセル数に依存します(画面の横幅と同じ数の光線を飛ばす). (図では光線の一部を省略しています)

f:id:JUN_NETWORKS:20210304085314p:plain

で, 光線を飛ばすといっても適当に飛ばしたのではどの壁にぶつかったかわからないので, DDA(デジタル差分解析)という方法を使って光線を1マスずつ進めていき, 壁にぶつかったかどうかを都度チェックします.

f:id:JUN_NETWORKS:20210304095819p:plain

Raycasting

sideDistX, sideDistY は最初プレイヤーがいる位置がぴったりマスの端にいるとは限らないのでまず最初の1回目だけは別でマップの端に移動するための量.

deltaDistX, deltaDistY はマスの端から次のマスの端までの距離.

これらの値を使って1マスごとに壁かどうかチェックしていき, 壁だったらそこで終了する.

壁までの距離を求める

光線を飛ばして壁にぶつかれば今度は, 壁までの距離を求めることが可能になります.

ただし, プレイヤーから当たった壁までの距離をそのまま使うと魚眼のようになってしまうので, 工夫する必要があります.

具体的にはプレイヤーから壁までの距離ではなく, カメラ平面(を無限に左右に伸ばした直線)に対して垂直になるような距離(以後"垂直距離"と呼ぶ)を取ります.

f:id:JUN_NETWORKS:20210304104932p:plain

Raycasting

上の図でいうところの緑色の線が垂直距離です.

壁までの垂直距離の計算式や図を用いた導出は Raycasting でされているのでここでは割愛します.

壁を描画する

壁までの垂直距離が求まったところで, その値を使って今度は壁の高さ(height_base)を求めます.

これはただ単に 基準となる壁の高さ / 壁までの距離 で求めることが出来ます.

この基準となる壁の高さ は別にスクーン幅そのまま使ってもいいんですが, 実はそのままスクリーン幅を使うと縦横比がスクリーン幅によって変わってしまうので, 変わらないようにするために  2*|\vec{plane}| の逆数を掛けます. 式は スクリーン幅 / (2 * plane_length) です. この式の意味としては, 「視野角が広くなるということは  \vec{plane} の大きさが大きくなるよね. 大きくなるということは描画された時に横の長さが一定じゃなくなるってことだから,  2 * |\vec{plane}| の逆数を掛ければ横の長さが元に戻るんじゃね?」 っていう感じ.

これで壁の高さが求められたので, あとは画面中央を基準に壁を描画すればOKです. テクスチャを描画する場合は描画している場所が壁全体の上から何%の位置にいるのかというのを求めて, それをもとにテクスチャ画像から色を取得してスクリーンに描画すればOKです.

スプライトの描画

github.com

そもそもスプライトとはなんぞやって感じなんですが, スプライトは障害物みたいな感じのやつです.

f:id:JUN_NETWORKS:20210304111022p:plain
この樽みたいなやつがスプライト

スプライトは壁の描画とは一味違ってて, よく言われる表現が「スクリーンにシールを貼った感じ」というもの. これは実際に動いている動画とかを見ないと多分わからないと思うのでこの記事の最初の方で貼った動画を見てちょ.

ただ, 正直どのようにスプライトを描画したら正解なのかがしっかりわかっている訳ではないのでもしかしたら間違ってるかもです.

というか壁の描画がプレイヤーから光線を発射し, それが壁にぶつかるまでの距離を元にどのように描画するのかを決めていたのに対し, スプライトの描画はプレイヤーとスプライトの位置関係を元に2D画像をどのような大きさでスクリーンのどこに描画するか計算するので, 壁の描画とは考え方がだいぶ違います.

技術的には 透視投影変換 というものらしいです. (42にいるゲーム開発者の方が教えてくれました)

mem-archive.com

処理の流れとしては

  1. 壁の描画フェーズにてスクリーン幅分の光線放出し, 壁までの垂直距離を求めたので, その各光線の壁までの垂直距離の情報を配列(zBuffer とする)として保持する.
  2. スプライトまでの距離(ユークリッド距離)を求めて, 距離が遠い順になるようにソート.
  3. スプライトの座標をマップ上の座標からプレイヤーからの相対座標に変換する.
  4. カメラ行列の逆行列を掛けてスクリーン上でのx座標と, プレイヤーからスプライトまでの深度(3D空間におけるZ軸)を求める.
  5. スクリーン上のx座標とスプライトまでの深度を元にスプライトを描画する幅や高さを計算する.
  6. スプライトの左端から右端にかけてスクリーンの各xに対して描画処理を行う. その際に zBuffer[x] がスプライトの深度よりも遠い (つまりスプライトが壁よりも手前にある) 場合にスプライトを描画する.

はい. むずいですね.

正直スプライトの計算に関しては元サイトの方でも説明が少なかったので理解するのが大変でした.

スプライトの描画に必要な情報の計算

スプライトの描画に必要な情報は以下の通りです.

  • スプライトの描画座標
  • プレイヤーからスプライトまでの距離

この2つです. 一応他にもスプライトの描画サイズなどもありますが, 他の情報はすべて上記の2つの値から計算されるので, この2つの値がもっとも重要なのです.

この2つの値の計算に 透視投影変換 の手法を使います.

透視投影変換は2つのフェーズに分けることができます.

https://i2.wp.com/mem-archive.com/wp-content/uploads/2018/02/%E3%82%AD%E3%83%A3%E3%83%97%E3%83%81%E3%83%A34.png

透視投影変換とは | NO MORE! 車輪の再発明

つまり以下の2つの処理に分けることが出来ます.

  • 外部パラメータ:世界座標系からカメラ座標系への変換
  • 内部パラメータ:カメラ座標系から画像座標系への変換

ちなみに...

  • カメラ: 今回の場合, プレイヤーの現在座標にあたる.
  • 世界座標系: 絶対的な位置. 今回の場合だとスプライトのMAP上での座標にあたる.
  • 外部パラメータ: カメラ行列の逆行列(後述)を掛けてカメラ座標系に変換する.
  • 内部パラメータ: カメラ座標系から画像座標系(スクリーン上での具体的な座標)に変換する

というように対応します.

世界座標系からカメラ座標系への変換

まず初めに世界座標系からカメラ座標系への変換を行います.

この変換を行うために, スプライトの座標を世界座標系(スプライトのMAP上での座標)からプレイヤーからの相対座標に変換するひつようがあるので変換します.

式は,  \vec{sprite} - \vec{pos} です. シンプルですね.

次に相対座標からカメラ座標系への変換を行います.

カメラ座標系への変換には相対座標に対してカメラ行列を掛けるという操作を行います.

カメラ行列ってなんぞや? って感じですが, カメラ行列とは以下のような行列のことです.


\left[
\begin{matrix} planeX & dirX \\ planeY & dirY \end{matrix}
\right]

そしてカメラ行列の逆行列は以下のような式になります.


\left[
\begin{matrix} planeX & dirX \\ planeY & dirY \end{matrix}
\right]

これを先程求めたスプライトのプレイヤーからの相対座標 (  \vec{pos'} とする) に掛けます.


\frac{1}{(planeX \times dirY - dirX \times planeY)} \times
\left[
\begin{matrix} planeX & -dirX \\ -planeY & dirY \end{matrix}
\right]
\left[
\begin{matrix} sprite'X \\ sprite'Y \end{matrix}
\right]
=
\left[
\begin{matrix} transX \\ transY  \end{matrix}
\right]

この式によって求められる  transX ,  transY は以下のような意味を持つ.

  •  transX : カメラから見て左右のスプライトまでの距離. スケールはマップと同じ.
  •  transY : カメラから見て前後(深度)のスプライトまでの距離. スケールはマップと同じ.

図にすると以下のような感じ.

f:id:JUN_NETWORKS:20210304123149p:plain

カメラ座標系から画像座標系へ変換する

ここではスクリーン上に描画する際に必要となる以下の値を計算します.

  • スクリーン上でのx座標
  • スプライトのスクリーン上での高さ
  • スプライトのスクリーン上での幅

まずスクリーン上でのx座標ですが, これは (int)(screenWidth / 2 * (1.0 + transX / transY)) で計算出来ます. 式の意味としては, まず transX / transY が, スプライトが画面左端にある時は -1, 中央の時は 0, 画面右端にいるときは1になります. これに1足して, 画面の横幅の半分を掛ければ具体的なスクリーン上でのスプライトの中心のx座標が求まります.

スプライトの高さと幅については壁の高さのときに使った height_base を使って height_base / transY とするれば求まります.

後はこれを描画すればOKです.

f:id:JUN_NETWORKS:20210304124246p:plain
計算過程(間違ってたら教えてください)

BMP画像の出力

www.umekkii.jp

基本的にこのサイトを見ながらファイルに各種情報を書き込んでいけばOK. Bitmapらくちん!

マップが閉じているかの判定

github.com

設定ファイルにて示されているマップが閉じていない場合はエラーを返さないといけないので, マップが閉じているかどうかの判定を行う必要があります.

"マップが閉じている"というのは具体的には, プレイヤーが歩いてマップ外部に到達不可能. つまり, 壁で覆われているということです.

この判定は floodfill というアルゴリズムを使えば可能です.

en.wikipedia.org

おまけ

アルファブレンディング

処理が重かったので関数を作っただけで使いませんでしたが, アルファブレンディングというものを実装しました.

ja.wikipedia.org

// アルファブレンディング
// dst: 背景, src: 前景
uint32_t   alpha_blend(uint32_t dst, uint32_t src)
{
    double     alpha;
    uint32_t   color;

    // 透明: 0.0, 不透明: 1.0
    alpha = -((double)(src >> 24) / (double)0xff - 1.0);
    color = 0x00000000;
    color |= (int)((src & 0xff) * alpha) + (int)((dst & 0x000000ff) * (1 - alpha));
    color |= ((int)((src >> 8 & 0xff) * alpha) + (int)((dst >> 8 & 0xff) * (1 - alpha))) << 8;
    color |= ((int)((src >> 16 & 0xff) * alpha) + (int)((dst >> 16 & 0xff) * (1 - alpha))) << 16;
    return (color);
}

やってることとしてはWikipediaの記事をそのままコードにした感じ.

f:id:JUN_NETWORKS:20210304124513p:plain
樽の背景が少し透けてる

何かに使おうと思って作ったけど, 結局使わなかったので供養.

完走した感想

完走した感想ですが, 楽しい課題でした. 3Dゲームの描画の計算のやり方(レイキャスティング)や設定ファイルをC言語でパースする方法などを知れてよかったです.

ただ, エラーハンドリングがめんどくさかった... mallocの成否の判定などを書いていると, PythonやJSの例外って便利な機能だなぁと思いました.

あと, 数学を使ったコードを久しぶりに書きましたが楽しいですね. 久しぶりに高専で使ってた線形代数のノート引っ張り出して色々思い出しました.

昨今3DゲームとかはUnityとか使えばある程度のものは簡単に作れるわけですが, 一度こうやってゲームエンジンなどを使わず, 原始的なゲームを作ってみると, かなりゲームエンジンに対する考え方なども変わりますし, ゲームエンジンってすげぇなぁ〜って思いました.

今回も記事を書く前はめんどくさいなぁ〜と思ったのですが, 記事を書く中で適当に理解して放置していた箇所を理解し直す羽目になった(特にスプライトのところ)ので良かったです. やはりアウトプットは大切. はっきりわかんだね.

本当はこの課題の前に, printf自作とかサーバー系の課題とか面白い課題が色々あったのですが, 結局書きたいなーと思って放置してしまっていますね... printfはそのうち書きます.

42, いい場所なので, プログラミングが好きな人間は来ると幸せになれると思います.

では今日はこのへんで. ではでは〜〜👋👋

「エンジニアの知的生産術」を読んだ.

「エンジニアの知的生産術」という本を読みました.

読んだ目的

知的生産を上げたかったから.

具体的には自分は本を読むのがめちゃくちゃ遅いのでその辺解決出来ないかなぁと思い読んだ.

各章のまとめ/感想

1章 新しいことを学ぶには

この章はこれから先の章で学ぶことを一通り紹介するような感じだった.

時間がない人はこの章だけ読んでもいいかも?

後にいくつかの章で言及されることだが, 基本的に質の良いインプットというのは, 「全体を把握した後, 細部を理解する」という感じらしい.

この章で大事なのは, 学びのサイクルは以下の3つの繰り返しであるということ.

  1. 具体: 情報収集や体験など
  2. 抽象: パターンの発見など. 抽象化.
  3. 応用: 抽象化したものを実際に使う(実践する, 検証する)

2章やる気を出すには

やる気が起きない人の多くはタスクが多すぎるかららしい.

なのでやるタスクを絞る必要がある.

タスクの絞り方としては, タスクや思っていることを全て紙に書き出して, それらを整理しこれから何をするかを決める.

これは Getting Things Done (GTD) と呼ばれる手法らしい.

postd.cc

何を優先するかという点においては「7つの習慣」の第3の習慣を参考にすると良い.

www.recruit-ms.co.jp

またタスクが大きすぎるとゴールが遠く, やる気が出ないので, タスクを細かいタスクに分割するか, 時間で区切るなどの工夫が必要.

所感

わかる...自分も数ヶ月前まで業務で設定するタスクの粒度が大きくやる気が出ないことが多かったが, ここ数ヶ月タスクの粒度を細かくすることによって業務効率が上がったことを実感している.

時間を区切るというのは具体的にはポモドーロテクニックなどだが, この辺は創造的なタスクなのか, 記憶のタスクなのかで向き不向きがあるというのをどっかの本で読んだ. (多分DAIGOの本だと思う) (ちなみにポモドーロテクニックは暗記などの創造性の低いタスクに向いてるって書いてた気がする)

3章 記憶を鍛えるには

記憶とはPCのメモリのようなものではなく, どちらかというと筋肉のように何回も鍛える必要がある.

記憶は使う(思い出す)たびに強くなる.

アウトプットをするのは思い出す行為にあたり, 記憶が強化される.

記憶を鍛えるためには反復学習が適している. 次の学習の時期は忘れた頃くらいが良いらしい.

このあたりの学習の期間の調整などの仕組みやソフトとして, ライトナーシステムAnki というソフトなどがある.

note.com

所感

つまるところ, いっぱい思い出しそう! アウトプットしよう!

しかし, この学習間隔というのがむずいよな...

"英単語を覚える" とかだったら忘却曲線に基づいた学習間隔で出題するアプリもあるっぽいしそういうのを使ってもいいいかも

4章 効率的に読むには

目的意識を持って本を読むと効果が上がる.

早く読もうとすると入ってくる情報量が理解する速さを上回ってしまい, 結果インプットの量に比べて理解出来た量が減ってしまう.

無理して早く読むよりも自分に適した速度で読むのが良い.

本の読み方としては - まずは本を読む前に本の全体像を把握する - 目次で章や説を見る - 書評などを探して感想を読む.

本を読むための目的はいくつかあり, 具体的には以下のようなものがある

  • 大雑把な地図を得るため: どの情報がどこにあるか把握し, 必要な時に読み返せるようにする
  • 結合を起こす: 既に持っている情報や他の本との間のつながりを見つけ出し, 新たな知識を得る
  • 思考の道具を得る: 具体的な言葉などを知りこれからその分野などでの学習や会話が出来るようにする.

これらの目的も素晴らしいが, 著者は 復習のための教材を作るを目的とすることをおすすめしている.

復習のための教材というは, "例えば人に教える" や "(人に教えるために) ブログやスライド, 資料などを作る" といったことが当てはまる.

ここでのポイントは 人に教える ということである.

人に教えるために作った資料などは後に自分が見返した時にも大いに役立つ.

所感

このブログがまさにそれだね!

また, 復習という意味では読み終わった後に他の人が書いた書評を読むのも自分とは違った意見や視点を知れてとてもおもしろく復習になるし新たな発見があったりしてとても良い.

アウトプットを前提に読むというところでは, 読み終わってから書評を書くのではなく, 各章読み終わるごとにその章の感想を書くのが良いのかなぁと思ったりした. (忘れちゃうので(思い出すことが大事なはずでは?))

このあたりはいくつかやり方を試してみて一番効果が高そうなものを行うようにすれば良いかな.

5章 考えをまとめるには

考えをまとめるにはKJ法というのが良い.

http://www.ritsumei.ac.jp/~yamai/kj.htm

流れとしては

  1. 持っている情報, 思いついたことなどを全て付箋に書き出す.
  2. 主観的に書き出した付箋で近いものを近い場所に置きグループ化する
  3. グループに名前を付ける(名前を付けれない場合はグループ分けが良くない)
  4. グループ間の関係(類似, 対立などなど)を矢印などで書く.
  5. 文章化してアウトプット

人によってやりやすいようにカスタマイズすれば良い.

6章 アイデアを思いつくには

著者によるとアイデアが思いつくまでには以下の3つのフェーズがあるらしい

  1. 耕すフェーズ
  2. 芽生えるフェーズ
  3. 育てるフェーズ

1.耕すフェーズでは情報のインプットや調査, つながりを見つけ出したりなどが含まれる. 第4章で紹介したKJ法なども大いに役立つ.

2.芽生えるフェーズでは耕すフェーズで獲得した知識や情報を脳内で寝かしておきアイデアが芽生えるのを待ちます. この辺は昔読んだ本にお風呂などのリラックスした状態では脳が思考や情報を整理を行う といったことが書かれていた. だから風呂でアイデアが思いつくことが多いらしい.

3.育てるフェーズでは, 芽生えたアイデアというものはそのままでは使えないことが多いのでそれをブラッシュアップしてプロダクトに出来るレベルにするみたいな感じ.

所感

ハッカソン(アイデアソン)に出た時に複数人でアイデアやワードを付箋に書いていって, そこからアイデアを生み出し, 実際に開発するということを何回かしたが, 今思えばその当時行ったことはまさにこれではないか.

確かにあの当時, 「何も思いつかん...」というところからアイデアを実際に生み出し, そして開発したことからしKJ法+この章の方法 はアイデアを生み出すことが出来そうだ! と思った.

こうやって自分の過去の経験と結びつくと記憶が強化されるらしい.

7章 何を学ぶか決めるには

1つの分野を極めるのも良いが, 複数分野の掛け合わせが強いぞ! という感じの内容.

他者が持つ知識と差別化を行い, 自分の知識の価値を高めよう. といった感じの内容.

所感

自分なんかは去年くらいにどの分野を極めればわからず悩みまくっており, その際に10人以上のエンジニアや人事の人と話した結果, 1つの分野を極めるだけがエンジニアではなく, 複数分野が出来たり, 他の業界の知識があることが強みになるということがわかった. まさにこの章の内容と同じだなぁと思った.

読んだ感想

有用なテクニックが学術的根拠と実例と共に紹介されており良かったと思う.

ただ, 自分はこういう本を読むだけで満足しちゃって実践しないことが多いのでしっかり実践しようと思った.

ホントはブログに書評を書く予定はなかったのだが, この本の内容を実践してみようということで書評を書いてみた.

KJ法などは実践するのが大変そうだなぁ...と感じた.

ただ, ハッカソンや個人開発において何か作りたいけどアイデアが出てこないときにはとても役に経ちそうだったので最低一回は実践してみる. (実践した感想をもしかしたらブログに書くかも)

書評に関してだが, 今回久しぶりに書いてみてそんなに手間ではない(1hくらいで書ける)のでこれから本を読む度に書こうと思う. またフォーマットはこの記事のフォーマットが自分に合っていてとても書きやすかったので次回以降このフォーマットを使って書くと思う.

この書評を書く前にScrapboxに感想や有用そうなことをまとめようと試みたが, ただ本文を移しただけみたいになってしまい, 労力に対してリターンが少なかったので止めた. この本にも書いてあったが, 人に教えるために書くことが大事ということで, やはりブログに書くのが一番だなと思った.

Toggl で時間配分を可視化することにした.

自分は多くの時間をネットサーフィンとTwitterYouTubeに費やしており, 生産性の低い毎日を送ってます.

現在ずっと実家におり, 可処分時間という点では高専在学時より増えているはずですが, どうも高専在学時ほど生産的な毎日を送っている実感を得られていません.

なので, 自分がどれくらい時間を使っているのか可視化するために Toggl Track というものを始めようと思います. (丁度この記事を書いているのは2月1日ということで月始めですし丁度良いですね)

toggl.com

使い方は以下の記事を参考に, ゆるく長く続けるためにTagは設定せず, 大雑把にプロジェクトを分けるような感じで使いたいと思います.

www.wantedly.com

また, 実は昔一度このツールを使ったことが合ったんですが, 1日で辞めてしまいました. その時の理由として

  • タスク入力がめんどい
  • タスクの粒度が荒くて結局何しているかよくわからない

みたいな感じだったので, 今回は

  • Chrome拡張, デスクトップアプリ, モバイルアプリ全て導入し, 心理的障壁を下げて気軽に入力できるようにする.
  • プロジェクトは大雑把に区切り, タスクは日本語で気軽に頭に思い浮かんだ言葉で書く. (考えるのは疲れるので)

という感じで, ゆるく長く続けて行きたいですね.

f:id:JUN_NETWORKS:20210201030230p:plain
プロジェクトの分け方はこのくらいの雑さの方が続くかな

f:id:JUN_NETWORKS:20210201030059p:plain
Chrome拡張便利です.

これが続いたら, 1ヶ月の振り返りとか書きたいですね.

では今日はこのへんで. ではでは〜👋👋

4Kモニタを買ったが使いこなせていない話

「素晴らしーーー!!!!」とか言っていますが, 確かにP2721Qは文句なしに素晴らしいです. 画質は綺麗だしベゼルは細いし, メニューは使いやすいし.

でも, 自分のPCのスペックが足りなくて快適4K生活が送れていないんだなぁ〜〜

P2721Q について

とりあえずモニタ本体について少し書きます.

www.dell.com

セールで4.5万円くらいで買いました.

以下の点で優れていると思います.

  • 画面が綺麗(4Kなので)
  • ベゼルが薄い
  • メニュー操作がとてもしやすい
  • 電源内蔵だからコードがスッキリ
  • 付属スタンドが優秀
  • USBハブ代わりになる.
  • 27インチ4Kにしては安い

ちょっと残念のポイントとしては

  • スピーカーが付いてない(まぁ付いてても使わんけど)
  • 下のベゼルが他の3辺に比べて太い (気にならんけど)

と言った感じでほぼ完璧です.

実際弟の MacBook Air (2020) とつないだ画面を見たときにはめっちゃ感動しました.

じゃあ何が駄目かというと自分のPC側なんすね〜〜

自分のPCで使いこなせない問題

自分のPCは Ubuntu on Thinkdpad X1 Carbon 2017 みたいな環境です. もちろんプロセッサ内蔵GPUです.

で, UbuntuMacのようにいい感じに倍率とか画面輝度とかフォントサイズとか調整してくれないのでその結果このようになります.

小っっっっっっっっっっっっっっっっっさ

というわけで本当にただ全ての要素が1/4のサイズになっただけなのでマジで見えないです.

で, 一応これには解決策があって, 画面拡大倍率を125%にして, Chromeの拡大率も125%にしたら一応ギリまともに見れるくらいにはなります.

f:id:JUN_NETWORKS:20210117015145p:plain

ただ, これをすると画面倍率を125%にしたせいか, PCの動作がめっちゃ重くなって, Twitterもまともにできないくらい重くなるので100%で我慢しているわけなのです.

これから

この先の予定としては

  • MacBookAir (2020) を買う
  • デスクトップPCを組む

の2択かなぁ... (もしくは両方)

M1チップのMBA, スペック的にはとても魅力的なんだけど, Virtualbox や Docker などの仮想環境が今の所使えないというのが痛すぎる... 仕事にならんし42Tokyoもできん... だからと言ってIntelMBAなどを買う気にはなれないんですわ...

デスクトップPCは前から一度組んで見たいとは思っていたので組むのありだけど, 時間と金がかかるのと, Macの方が結局開発環境として優れているんだよな. まぁUbuntuとのデュアルブートにすれば良いという話かもしれんが. (WSLで開発できそうならWindowsだけでもいいかもしれん)

まぁどちらにせよまた何か進展があったらブログに記事とか書くと思います.

ではでは.

2020年振り返り

激動の2020年も今日で終わり!! なので毎年恒例1年の振り返りをするんじゃ!

去年はこちら

jun-networks.hatenablog.com

今年もツイートを漁りつつ, 思い出して書いてきます.

では早速やっていくぞー!

月ごとの振り返り

1月

  • 自分がバイトしている会社でインタビュー記事が公開されました.

www.wantedly.com

  • 計算理論の本を読んでオートマトンとか字句解析とかその辺について少し勉強していました. (結局この本は1/3くらいまで読んで放置してしまったので, またいつか読み直したい...)

  • 春高専カンファレンス初風 に行ったしましたね.

2月

jun-networks.hatenablog.com

読んだけど結局生活に活かせているか言われればあまり生かせれていないですね...

それよりも, 書評はブログには書いていないけれども, アウトプット大全という本がかなり良くて, 自分の今のアウトプットの考え方やアウトプットの仕方に影響を与えてくれたので良かったです.

jun-networks.hatenablog.com

3月

  • 42Tokyoの入学試験が新型コロナウィルスのせいで延期になりました.

  • N予備校プログラミング入門コース修了しました.

jun-networks.hatenablog.com

  • 少しだけAtCoderしてました

  • 30日OSにも手を出したものの1週間ほどで離脱してしまった...

4月

  • 新型コロナウィルスのせいで高専の授業がオンラインになりました.

  • リングフィットを買いました.

  • iPad Pro を買った
  • AirPods Pro を買った.
  • MacBookAirを買った.

5月

  • MacBookAirを売った

理由としてはとにかく動作が重くてまともに作業出来なかったからです.

  • MacからUbuntuに戻るにあたりOSをUbuntu20.04にして新たな環境にした.

6月

楽天カード, ポイント付くのは良いが, 正直デビットカードの方が即時に請求が来るので家計簿が付けやすいんだよな...

  • Teraformを業務で触ったりしていました.

  • AWSの設計などについても勉強して, この月で少しAWSに詳しくなった気がする.

7月

  • 家電制御プログラムの作成 (現在放置してて未完成)

github.com

  • "100分de名著"シリーズの「カント純粋理性批判」を読んだ. よくわからなかった.

8月

  • 42Tokyoの入学試験を受けた.

  • 名古屋歩いてたらチャリに轢かれてスマホ壊れた.

9月

めっちゃ楽しかったし, めっちゃいい会社でした. 制度や仕組みなども整っていてリモートでも働きやすくて素晴らしかったです.

使った技術としてはRailsとReactでした.

  • 42Tokyoに合格した.

jun-networks.hatenablog.com

  • 3分間ネットワークを一通り読んで, ネットワークの知識を付けた.

www5e.biglobe.ne.jp

このサイト, FlashPlayer使われまくってるので, 来年以降どうやって見よう...

10月

  • 久しぶりに電子工作して楽しかった.

  • リングフィット1週目クリアした.

  • Oculus Quest買った. VRC楽しい.

  • Nand2Tetris の課題の1つでアセンブリをバイナリに変換するプログラムをGoで書いた.

github.com

11月

  • GoでAPIサーバー書いてた.

  • どの4kモニターを買うか迷いまくって10時間くらい無駄にした

  • VSCode使いからVimmerになった. (nvim)

  • ハッカソン出ようと思ったけど作品が期限までに完成せず諦め.

  • 業務でReactを触ることになったのでReact勉強した.

jun-networks.hatenablog.com

12月

  • 42関連で2つ記事を書いた.

jun-networks.hatenablog.com

jun-networks.hatenablog.com

  • 42のおかげで低レイヤーに詳しくなった.

    • CPUの実行効率
    • メモリアクセス効率
    • 標準Cライブラリの実装
  • 生活リズムが崩壊してて多くの時間を虚無に費やしてしまった.

  • スタンディングデスクを買った.

  • クリーンアーキテクチャの考え方を少し知った. (コードに落とし込めるとは言っていない)

全体の振り返り

今年は短期的な視点から見れば最低, 長期的な視点から見れば上々といった感じですね.

短期的な視点では, 完遂出来たことがとても少なく, 自己嫌悪に苦しめられました.

長期的には, 42Tokyoに合格出来たということで, これから楽しくなってきそうです.

去年の目標の達成具合

ちなみに去年の1年振り返り記事の中で

2020年は人生の大きな転換期になる気がしています.

とりあえず今ある2020年の目標は

  • CPU, コンパイラ, OSを作る (つまりはシステムプログラミング!)
  • 自分のポートフォリオサイトを作成する.
  • 毎日充実した日々を送り, 好きなことをいっぱい勉強する.
  • いろんな分野をやってみて自分の進みたい道を探す
  • 定期的に運動する.
  • 42Tokyoに受かる!!

今年は低レイヤーなどを中心とした含めたCS系の基礎固めの年になりそうです.

プログラミングを初めて3年くらいが経とうとしていて, 最近になってやっとこCSといものを知りしっかりと勉強したいと思うようになりました. 今年は"勉強したい"ではなく"勉強する"年に出来たらいいなと思います. そして来年のこの時期に1年の振り返りをした時に今年よりも充実し満足し幸せな1年を送れたらいいなと思います.

って書いてありますが, 最終的には以下のようになりました.

  • CPU, コンパイラ, OSを作る (つまりはシステムプログラミング!) → Nand2Tetris, 30日OS, 両方とも途中で放置. CPU作りは結局何もやっていない.
  • 自分のポートフォリオサイトを作成する. → 何もしてない.
  • 毎日充実した日々を送り, 好きなことをいっぱい勉強する. → 自堕落に過ごして非生産的な時間が多かった.
  • いろんな分野をやってみて自分の進みたい道を探す→ MLから離れて, クラウドインフラ, サーバー, 低レイヤー, フロントエンド と多くの分野を触った. しかし, 結局自分の進みたい道はわからなかった.
  • 定期的に運動する. → リングフィットのおかげで達成.
  • 42Tokyoに受かる!! → 受かった.

と, 基本的に何かしらに手を出したはいいものの途中で放棄してしまうことが多い一年でした.

何かしようと思いハッカソンに出ようとしたものの結局期限までに完成せず出られませんでしたし, 本当に中途半端で完遂しない1年でした.

ただ, 良いことも少しはあって, 念願の42Tokyoに合格したのは今年のマイナスを全て帳消しにするくらい良かったです.

今年の反省点としては

  • 家にいる時間が長く, だらだらする時間が多くなってしまい, やりたいことに時間を割けなかった.
  • 興味の対象がコロコロ変わるせいで何かを完遂せずに別のことをしてしまい, その結果完遂できなかったという結果のみが残り辛くなった.
  • Twitter, YouTube, ネットサーフィンに時間を費やしすぎた.
  • 生活にメリハリがなく惰性で生きてしまった.

と言った感じです. 完遂継続が出来なかった年でした.

よかった事

今年の振り返り記事はあまりにも暗すぎるのでここで頑張って明るい話題も探したいと思います.

42Tokyoに合格した

今年良かったことといえばとりあえずこれです.

ずっと独学でカリキュラムとかも特になく書籍とWebサイトを頼りにほぼ1人で勉強していたプログラミングですが, ついにカリキュラムとコミュニティを見つけました.

最近書いた標準Cライブラリのstrlen()の実装についての記事も42Tokyoの学生と一緒に色々試しながら書きました.

jun-networks.hatenablog.com

やっと自分のプログラミングの話を理解してくれて, 一緒に手を動かしてくれる人が出来てとても嬉しいです. (42Tokyo入る前にも2,3人くらいいたけども)

プログラミングの話ができて, ちゃんと理解してくれる(理解しようと努めてくれる)人が多くいるのは素晴らしいですね.

React触った.

今年の後半はもっぱら Typescript + React を触ってました.

Reactは数年前のインターンシップで触った時に難しすぎて挫折したフレームワークですが, 今年触ってみたら書きやすくなっていてびっくりしました. おそらくクラスベースから関数コンポーネントベースにReactの主軸が移ったからというのもありますし, 単純に自分が色々と経験を詰んだので理解できるようになったというのもあると思います.

Reactは最初何も考えずVueと同じような感じで1コンポーネントに色々詰めていたのですが, "りあクト!"という本を読んでそのやり方が間違いだと気づけて, 今年の終わりくらいにはちゃんとコンポーネント設計などを意識したテスタビリティが高いコードが書けるようになったかなぁと思います. "りあクト!" おすすめです.

jun-networks.hatenablog.com

低レイヤーを触った.

去年立てた目標にまでは達しませんでしたが, 去年の自分に比べてかなり低レイヤーに詳しくなったと思います.

42Tokyoのカリキュラムではしばらくは低レイヤーが続きそうなので, それらの課題を楽しみつつ, 趣味でCPUやOS作成など出来たらいいなぁと思います.

触った技術など

Python

コンテナで動かすサーバープログラムをFlaskで書いた

AWS

初めてAWSを使ったクラウドインフラの設計というのをしました.

VPC, ECS, CloudWatch, SageMaker, EC2, ECS, SQS, Lambda など沢山のサービスを調べて, 使いました.

Terraform

Infrastructure as a Code を実現するためTerraformを書いてました.

TypeScript

React書く用

React.js

今年初めて本格的に勉強して書き始めましたが, かなり書けるようになったと思います.

しかし, Atomic Design の分け方の粒度とかその辺はまだしっかりと理解出来ていないです.

Next.js

今年はReactを書いたという話をしましたが, React単体で使うことはほとんど無く, 実際にはNext.jsを使うことが多かったです.

Next.js, 色々と便利なんですよね...

C++

今年の初めAtCoder用に書いてた.

C

今年の下半期, TypeScriptと並ぶくらい書いた言語です.

42の課題は低レイヤーが多く, 序盤の多くの課題はC言語で書くことになります.

Makefileなども初めて書きましたが, めっちゃ便利ですね.

多分2021年もかなりの時間C言語を書くことになりそうです.

Go

今年勉強しはじめて, APIサーバーなどを書いていました.

Goは書き方が今まで触ってきた言語と結構違うのでどうやって書いたらいいかわからないんですよね...

2021年の目標

目標は高くすると年末に振り返った時に悲しくなるので2021年は今年の反省も踏まえ以下のように設定したいと思います.

  • 生産性の高い日々を送れるように努力する. 具体的には1ヶ月単位で目標を立てて, それを達成するような方針で生きる. (もしかしたら変わるかも)
  • Twitter, YouTube などを触らないようにし, メリハリのある生活をする.
  • 30日OS本, Nand2Tetris, アルゴリズムイントロダクションを完遂する.
  • 42Tokyoの退学システム(通称ブラックホール)に飲まれないようにする.

2021年は振り返った時に, 完遂したものが今年より多くなれるように頑張りたいと思います.

2020年買ってよかったもの

#買って良かった2020

今年から1年の中で買ってよかったものを紹介したいと思います.

ランキング形式みたいなのは苦手なので, 特に順位などは決めず列挙して一言添えるだけですがw

ではではスタートです.

iPad Pro (10.5インチ)

これはマジで買って良かった. QoL爆上がりです.

とにかく電子書籍が快適過ぎます.

iPad Pro で電子書籍読むの快適過ぎてやばいです. 電子書籍否定派から賛成派になりました.

ただiPad Proで電子書籍を読む際の欠点があるとすれば, 他のアプリに気を取られて本を読み始めるのが難しくなることですかね. なのでiPad Proで電子書籍を読む際にはiPad Proのロックを解除する前に「俺は本を読むぞ!」という強い決意を抱いてロックを解除しなくてはいけません.

あと, 積読が見えにくいのでいつの間にか読んでない本が溜まりがちですね

NINTENDO SWITCH & リングフィットアドベンチャー

任天堂は神.

今年は新型コロナウィルスの影響で殆ど家から出ておらず, ずっと椅子に座ってパソコンばかりしているのに太らず, むしろ筋肉量が増えて痩せたのはリングフィットのおかげです.

ありがとうリングフィット. ありがとう任天堂.

モンダミン

自分は1ヶ月のうち2週間は口の中に口内炎があるという, えげつないくらい口内炎ができやすい体質なのですが, 去年の終わりくらいからモンダミンを毎日歯磨きの後にするようになってから口内炎できる頻度が3ヶ月に1週間ほどとめちゃくちゃ改善されました.

口内炎軟膏やサプリメントなど数年間に渡り色々試してきましたが行き着いた答えはこれです.

ちなみに自分が使ってるのはモンダミンプレミアムというやつで, 1本800円くらいするんですが, 月額800円で口内炎の苦しみから解放されるなら安いもんです. (ドラッグストアだともっと安いかも?)

完走した感想

1年振り返り記事の執筆を完走した感想ですが, 暗い.

毎年振り返り記事を書くのですが, 例年なら「今年何もやってないと思ったけど, 意外と色々やってんなぁ.」って感じですが, 今年は「ほんまに何もやってへんやん...1年なにしてててん...」という逆のパターンになってしまいました.

いや, 暗いわ. 最後の感想まで暗いわ. もう少し明るくしようや.

来年は明るくて眩しいくらいの振り返り記事が書けるように努力したいですね.

では今日はこのへんで. ではでは〜👋👋