今日も技術的な話で、前回サラッと流したAccepted Speedを少し深堀りしようかと思います。
まず、プログラムというのは低級な物は全て無限ループで実装します。
Windowsプログラムからイベントドリブンという物が主流になったので、エンジニアでも隠されている部分を知らない人も多いかもしれません。
例えば、このボタンがクリックされた時というイベントに処理を書きますよね?
このボタンがクリックされた時という判定を行っている部分がMFCのようなフレームワークです。
無限ループ+Windowsメッセージを自前で全部処理して実装するというプログラミングも実は可能であり、(Windows95だっけかな?)が出た頃にはそういったコアな作り方の説明をしている書籍も存在しました。
脳みそはプログラムに書かれた通りの処理しかしないので、ぐるんぐるんループしてると1つの計算しかできません。
ですが、マイナーソフトを見ているとShare #999 acceptedが固まって出たり、明らかにパラレルに計算しています。
これがスレッドという概念で部下にこれやっておいて!と処理を任せてるイメージになります。
また、みんな大好きオープンソースのethminerのソースコードで説明します。
main.cpp
int main(int argc, char** argv)
{
・・・ 省略 ・・・
cli.execute();
cout << endl << endl;
return 0;
main関数が処理の開始なのはルールなのでそういうもんだと思って下さい。
中を見ると引数のオプションを最初にチェックしてMinerCLIのexecute()を実行しています。
MinerCLIも同じmain.cppの中に定義されています。
main.cpp
void execute() {
・・・ 省略 ・・・
// Initialize Farm
new Farm(m_DevicesCollection, m_FarmSettings, m_CUSettings, m_CLSettings, m_CPSettings);
// Run Miner
doMiner();
}
executeのケツの方でdoMinerを実行しています。
その上のFarmオブジェクトがGPUを管理してるやつなので、デバイス情報を先に設定しています。
main.cpp
private:
void doMiner()
{
new PoolManager(m_PoolSettings);
・・・ 省略 ・・・
// Start PoolManager
PoolManager::p().start();
doMiner()の中でPoolManagerを開始します。
PoolManager.cpp
PoolManager::PoolManager(PoolSettings _settings)
: m_Settings(std::move(_settings)),
m_io_strand(g_io_service),
m_failovertimer(g_io_service),
m_submithrtimer(g_io_service),
m_reconnecttimer(g_io_service)
{
DEV_BUILD_LOG_PROGRAMFLOW(cnote, "PoolManager::PoolManager() begin");
m_this = this;
m_currentWp.header = h256();
Farm::f().onMinerRestart([&]() {
cnote << "Restart miners...";
if (Farm::f().isMining())
{
cnote << "Shutting down miners...";
Farm::f().stop();
}
cnote << "Spinning up miners...";
Farm::f().start();
});
PoolManagerのコンストラクタでFarmのonMinerRestartイベントハンドラを設定します。
多分apiのminer_restartがトリガーなんだと思うけど、とりあえずFarmのstartが起動される。
Farm.cpp
bool Farm::start()
{
・・・ 省略 ・・・
// Start all subscribed miners if none yet
if (!m_miners.size())
{
for (auto it = m_DevicesCollection.begin(); it != m_DevicesCollection.end(); it++)
{
・・・ 省略 ・・・
// Initialize DAG Load mode
Miner::setDagLoadInfo(m_Settings.dagLoadMode, (unsigned int)m_miners.size());
m_isMining.store(true, std::memory_order_relaxed);
else
{
for (auto const& miner : m_miners)
miner->startWorking();
m_isMining.store(true, std::memory_order_relaxed);
}
初回はDAG生成、次回はstartWorking実行。
Worker.cpp
void Worker::startWorking()
{
DEV_BUILD_LOG_PROGRAMFLOW(cnote, "Worker::startWorking() begin");
・・・ 省略 ・・・
m_work.reset(new thread([&]() {
setThreadName(m_name.c_str());
// cnote << "Thread begins";
・・・ 省略 ・・・
try
{
workLoop();
}
ここでスレッドが登場しています。
workLoopをスレッドとして実行していますが、このworkLoopはオーバーライド用メソッドなので参照先プログラムが異なります。
CUDAしか興味ないんでCUDAMinerしか見なくていいですけど、随分前にどっかで書いたと思いますが、CUDAコアにこれ計算して!ってお願いする部分だからOpecCLとかCPUで処理が違う訳ですね。
なので最初の方にm_DevicesCollectionに突っ込んでる時もenumDevicesのクラスもデバイス種別で別れてた訳です。
AMDのグラボはCLMinerに入って、NVIDIAのグラボはCUDAMinerに入ります。
CUDAMiner.cpp
void CUDAMiner::workLoop()
{
・・・ 省略 ・・・
// Epoch change ?
if (current.epoch != w.epoch)
{
if (!initEpoch())
・・・ 省略 ・・・
// Eventually start searching
search(current.header.data(), upper64OfBoundary, current.startNonce, w);
searchが実質的な計算部分だと思いますが、要はスレッドとしてこれが複数実行されている訳です。
最初に脳みそは書いた通りに1つしか実行しないと言ったように、スレッドで部下に任せた部分は関与せずに脳みそはメインの処理に戻ります。
つまり先程のstartWorkingをm_miners(デバイス数)分ループしていたFarm::startはメインの脳みそで実行されて複数のスレッドを生み出す訳です。
main.cpp
// Stay in non-busy wait till signals arrive
unique_lock<mutex> clilock(m_climtx);
while (g_running)
g_shouldstop.wait(clilock);
#if API_CORE
// Stop Api server
if (api.isRunning())
api.stop();
#endif
if (PoolManager::p().isRunning())
PoolManager::p().stop();
さらにmain.cppのdoMinerに戻ってくるとwhile(g_running)で無限ループで待機して、ctrl+c等で終了したら後始末でapi.stopとPoolManager.stopを実行する訳です。
スレッドで実行している部分は裏でOSがCPUを割り当てていて、どのスレッドが早く終わるか?優先的にCPUを割り当てられるか?は分からない。
CUDAコアへの計算指示待ちの時間もバラバラ。
なので同じ難易度の計算でもハッシュレートにも僅かばかりのばらつきが出たりする訳で、Accepted Speedの集計期間。
例)20:00-20:05の間と20:05-20:10の間など(前回記事で書いたのはこの期間が不明)
集計期間中に処理されたナンス数にはバラツキがあるので上振れしたり下振れしたりする訳です。
例えば、20時05分1秒(期間の初め)にShare #999 Acceptedで大量にプールにshare報告したら、その期間では大きく上振れするという事が容易に想像できますよね?
とりあえず今日はここまで!
コメント