LoginSignup
345
165

More than 3 years have passed since last update.

何度も"HelloWorld!"を出力するとスキルが伸びるらしい

Last updated at Posted at 2020-10-29

あらすじ

先日このような記事をTwitterで見つけた。
プログラミング言語別『Hello World!』で違いを比較してみる
この記事ではタイトル通り、いろいろなプログラミング言語でのHello World!の出力方法が書かれている。

C言語だと、!を表現するのに『\n』を使います。

など、純粋に間違えていることも書かれているのだが、Twitterで有名になっていたのは最後のまとめの部分である。

とにかく何度も『Hello World!』といったような文字列を出力することで、プログラミングスキルは伸びますよ。

......

プログラミングを経験したことがある方は鼻で笑うくらいには笑われたことかと思う。
話題のブログを見ればわかるが、Hello World!と出力するくらいのプログラムは、大体どの言語でも数行程度でかけるし、
何ならコピペでもいい。

いやでもまぁ、Hello World!を何度も出力するだけでスキルが伸びるなら夢のような話である。

そこで、c++を用いて何度もHello World!を出力してみようと思う。
当方++初心者も初心者で、右も左もわからない状態なので、寝てる間にたくさん出力して起きたときに「c++チョットデキル」といえるくらいスキルアップしていればなぁ......

ご注意

この記事に書いてあることは初心者も名乗れないレベルの素人が書いたものです。
間違っていることも多いかと思いますので、その際はご指摘いただけますと幸いです。

開発環境

たまたま手元にあったg++とcmakeを用いる。
今のところ特にライブラリを使おうとは思ってないが、後程追加するかもしれない。

出力の定義

これはある程度書き終わった後に書き加えているのだが、
この記事ではprintf関数、もしくはそれに似たような関数の実行をもって出力とする。
画面に文字が出ていることだとか、コンソールのフラッシュだとか、そういったことは出力の定義に含めないことにする。

本編

まずは簡単なものから。

#include <iostream>

int main() {
    std::cout << "Hello World!" << std::endl;
    system("pause > nul");
    return 0;
}

出力結果はもちろん

    Hello World!

である。

でもこれでは1回しか実行されず、スキルアップの幅が大きいとは思えない。
とりあえず100万回くらい出力したい。
ちなみに当方はusing namespace std;は書かないタイプ。

100万回出力されたHello World!

特に何も考えず100万回のループを作ってみた。

#include <iostream>

int main() {
    //c++14から数値リテラルに桁区切り文字としてシングルクォーテーション(')が使用できることになった。便利。
    for (int i = 0; i < 1'000'000; ++i) {
        std::cout << "Hello World!" << std::endl;
    }
    system("pause");
    return 0;
}

大量にHello World!が出力され、そこそこスキルアップしたと思うが、待てる時間だったのでスキルアップの幅はまだまだ小さいだろう。
具体的にどのくらい時間がかかっているのか、STLchronoを使って、ループ部分の実行時間を調べてみる。

出力部の実行時間の計測

#include <iostream>
#include <chrono>

using namespace std::chrono;
using hr = high_resolution_clock;

int main() {
    //現在の時間を記録
    const auto t = hr::now();
    for (int i = 0; i < 1'000'000; ++i) {
        std::cout << "Hello World!" << std::endl;
    }

    //今の時間からforループの前に記録した時間を引くことで経過時間を計測。
    //duration<float>というのはchronoで定義されているsecondsをint型からfloat型にしたもの。ミリ秒以下が切り捨てられない。
    const auto d = std::to_string(duration_cast<duration<float>>(hr::now() - t).count());
    std::cout << d << "sec" << std::endl;

    system("pause > nul");
    return 0;
}

結果、実行時間は98.915352秒だった。
長すぎる。100万回くらいもっと速く出力してもらわないと「c++チョットデキル」といえるくらいになるには死んでしまう。
出力について調べてみると、endlというのが遅く、改行したいだけなら単純に\nを書いたほうが速いそう。
また、iostreamを使った出力がそもそも遅いらしく、printfのほうが速いらしい。
ということで書き換えてみる。

出力関数をprintfに変更

#include <stdio.h>
#include <chrono>

using namespace std::chrono;
using hr = high_resolution_clock;

int main() {
    const auto t = hr::now();

    for (int i = 0; i < 1'000'000; ++i) {
        printf("Hello World!\n");
    }

    const auto d = duration_cast<duration<float>>(hr::now() - t).count();
    printf("%fsec.", d);
    system("pause > nul");
    return 0;
}

出力された実行時間は39.244164秒。結構速くなった。
iostreamをインクルードしたままだと400秒くらいかかり、インターネットの情報は容易に信じてはいけないなと思っていたが、stdio.hに変えたところ、この速度になった。何が違うんだよ。

でも、まだまだ速くしたい。正直10秒くらいで出したい。
調べているとバッファリングというものを発見した。
大雑把に言うと、一旦データをためて一気に出力するという方法らしい。とりあえず書き直してみる。

バッファリング

int main() {
    //バッファリングを決める関数。第一関数に渡されたストリームをバッファリングする。
    //_IOFBFはフルバッファリングを意味する。
    //第三引数はバイト単位。
    //2 * 1024 * 1024byteは2MiBである。今回2MiBなのはなんとなく。
    setvbuf(stdout, NULL, _IOFBF, 2 * 1024 * 1024);

    const auto t = hr::now();

    for (int i = 0; i < 1'000'000; ++i) {
        printf("Hello World!\n");
    }

    const auto d = duration_cast<duration<float>>(hr::now() - t).count();
    printf("%fsec.", d);

    fflush(stdout);

    system("pause > nul");
    return 0;
}

出力された実行時間は8.510801秒。めちゃくちゃ速くなった。
正直ここで満足してもよいのだか、もうちょっとやれることをやろうと思う。

改行を消す

まず改行を消す。Hello World!を出力することが大事なので改行は必要ないと判断。速度が変わらなければ戻そう。
ソースコードは\nを削除するだけなので割愛する。
出力された実行時間は1.636904秒。マジかよ。まためちゃくちゃ速くなってしまった。
まぁ出力されている画面を見ていれば明らかに改行が邪魔だったので、速くなることは想像できたが、まさかこんなに速くなるとは。

ちゃれんじ1億回

速くなったところでひとまず実行回数を100倍にしよう。1億回にチャレンジです。
実行時間は177.972702秒。177倍!?とおもって慌てて電卓で計算したら108倍だった。
理由はわからないが100倍以上になったのは確かである。

でっかくなっちゃった!(バッファサイズが)

バッファサイズを大きくしてみる。当方のPCはメモリを32GBも積んでいるのである程度大きくしても多分大丈夫。
とりあえず4GiBにしてみたところ、メモリの確保に失敗した。
理由はわからないまま1GiB(1024 * 1024 * 1024)にしてみたところ出力された実行時間は160.617279秒。ちょっと速くなった。
1.10倍の差ではあるが、秒数にして17秒の差はかなりでかい気がする。気がする。

putsについて

出力についてまた調べていると、putsという文字列を出力することだけに特化した関数の存在を知った。
printfは変数を書式化したりという処理が含まれるため、とくに処理を挟まないputsのほうが速いそう。
だが、putsでは改行が必ず入るそうなので却下。

並列化

タスクマネジャーを見ながら実行してみたところ、スレッドが一つしか使われていないことに気づいた。
16個のスレッドがあってなぜ1つしか使わないのか。全部使いたいものである。
「c++ マルチスレッド」などで調べたが、forループを簡単に並列化する方法と出会えず。分割は面倒なのでしたくない。

そんな時、謎の声が聞こえた。
「OpenMPって知ってる?」
「───Open、MP?」
......ちょっと待った。
OpenMPっていうのは、並列計算機環境において共有メモリ・マルチスレッド型の並列アプリケーションソフトウェア開発をサポートするために標準化されたAPIの名だ(出典)。
OpenMP ARBによって開発されクロスプラットフォームで様々な環境で動くAPIであり、例外処理に優れているとされるTBBとここ5年間戦い続けているがもう古いとされつつあるAPI。

使ってみるか......
ただ、どう考えたってバッファリングと仲が悪そう過ぎる。
バッファリングっていうのは大きい配列を用意してそこにどんどんためていくだけだから、
タイミングによっては同じ場所に書き込もうとする可能性がある。

openmpはループの上に#pragma omp parallel forと記入し、コンパイルオプション-fopenmpを追加するだけで並列化でき、
比較的簡単なのでこちらもソースコードは割愛させていただく。

とりあえず実行してみたところ187.399261秒かかった。誤差かもしれないと思ってやり直したら196秒くらいかかった。
伸びてしまった。

理由がわからず歯がゆいところだが、ひとまず並列化は断念することに。

GUI

GUIの文字表示は速いんじゃないかと思った。なんとなくそう思っただけなのだがやってみないと気が済まない。
GUIの実装にはQtを用いる。g++と一緒に手元にあったからである。
Qtを用いた場合のビルド方法だが、CmakeLists.txtに以下の文を追加する。

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

find_package(Qt5 COMPONENTS Core REQUIRED)
find_package(Qt5 COMPONENTS Gui REQUIRED)
find_package(Qt5 COMPONENTS Widgets REQUIRED)

file(GLOB_RECURSE UI_FILES src/*.ui)

//追加
add_executable(
#~~~メインのソースコードとか~~~
${UI_FILES}
)

//追加
target_link_libraries(
#~~~Qt以外のライブラリ~~~
Qt5::Core Qt5::Gui Qt5::Widgets
)

これでおそらくビルドできるはずだ。あまり自信がないので詳しくは調べてほしい。

まずは純粋に出力してみようかと思ったが、バッファリングを分けて実装するのは面倒だったので、同時に実装する。
Hello World!が出力に対応する関数に渡されるたび、クラスのメンバ変数logに書き加えていき、
logを定期的に出力する。という方法をとる。

//追加になったヘッダ
#include "ui_widget.h"
#include <QtWidgets/QWidget>
#include <QtWidgets/QApplication>
#include <QtWidgets/QStyleFactory>
#include <QtCore/QtCore>
#include <QtGui/QtGui>
#include <mutex>
#include <thread>

void Widget::work() noexcept {
    const auto t = hr::now();
    for (int i = 0; i < 100'000'000; ++i) {
        print("Hello World!");
    }
    const auto d = std::to_string(duration_cast<duration<float>>(hr::now() - t).count());
    print("\n"); print(d.c_str()); print("sec.");
    flush();
}

void Widget::print(const char* str) noexcept {
    log += str;
    //文字数が2^30に近くなれば出力し、logを空にする。
    if ((512 * 1024 * 1024) - static_cast<int>(log.length()) <= 48) {
        display();
        log.clear();
    }
}

void Widget::display() noexcept {
    console->setText(log);
    QTextCursor c = console->textCursor();
    c.movePosition(QTextCursor::End);
    console->setTextCursor(c);
    console->update();
}

consoleという名前のQPlainTextEditを用意し、そこに出力していく。

出力された実行時間は34.917381秒。鬼のように速くなったが、QtのQString型は2^30文字強しか保存できないらしい。
std::pow(2, 30)はコンパイルの際定数にならないと思うので、512 * 1024 * 1024と書いた。
※GCC 4.6.1以降では、独自拡張としてconstexpr指定されているそうです(出典)

圧倒的に高速になったが、GUIには一文字もHello World!の文字が表示されず、秒数だけが表示された。
ループ回数を500万回にすると表示されるようになった。なんで?

あとQPlainTextEditの中がめっちゃ重い。文字数が多いからかな。

試みとして、
print関数を別のスレッドで実行しつつ、logの文字数が上限に近くなればシグナルを発出し、
メインスレッドで画面に表示する関数を実行するようにしてみる。

Widget::Widget(QWidget *parent) noexcept : QWidget(parent) {
    setupUi(this);
    setStyle(QStyleFactory::create("windowsvista"));
    connect(this, SIGNAL(changeLog()), this, SLOT(display()));

    workingThread = std::thread(work, this);
}

void work(Widget* ui) noexcept {
    const auto t = hr::now();
    const QString Hello = QString("Hello World!");
    for (int i = 0; i < 5'000'000; ++i) {
        ui->print(Hello);
    }
    const auto d = QString::number(duration_cast<duration<float>>(hr::now() - t).count());
    ui->print(QString("\n") + d + QString("sec."));
    //メンバ変数にstd::mutex型のmtxという変数を定義しておいて、それをロックすることでGUIをマルチスレッドで操作する。
    //メンバ関数もリソースの一つであるため、呼び出すにはロックしておいたほうが無難。いらないかも。
    std::lock_guard lock(ui->mtx);
    ui->flush();
}

void Widget::print(const QString str) noexcept {
    std::lock_guard lock(mtx);
    log += str;
    if ((512 * 1024 * 1024) - static_cast<int>(log.length()) <= 48) emit changeLog();
}

void Widget::display() noexcept {
    console->appendPlainText(log);
    QTextCursor c = console->textCursor();
    c.movePosition(QTextCursor::End);
    console->setTextCursor(c);
    console->update();
    log.clear();
}

void Widget::flush() noexcept {
    emit changeLog();
}

void Widget::clearLog() noexcept {
    log.clear();
}

出力された実行時間は0.172648秒。最高。イェイ。
ただこれはループ部分の実行時間でdisplay関数待ちで20秒くらい待つことになる。
display関数の実行時間も測ってみると18.2792秒もかかっていた。コンソールに出力するのとどっちが速いんだこれ......

VSコンソール

コンソールでも500万回出力してみる。もちろんバッファリングはあり。
結果はフラッシュの時間も合わせて10.711688秒。そっちのほうが速いんかーい。
時間をめちゃくちゃ無駄にしました。丸一日費やしたのにな。

結局コンソール

コンソール出力でループ部分をwhile(true)に書き換える。
無限ループにして寝る。これで当方は明日起きればc++の仕様をすべて頭の中に入れ「c++チョットデキル」といえるようになる。
一応今後これを真似する人のために、参考程度に一晩で何回出力できるか数えておきたい。
当方が知っている一番大きな値を保存できる型はunsigned long longだが、最大値は18,446,744,073,709,551,615(1844京6744兆0737億0955万1615)である。
正直十分に大きいがそんな範疇で収まってほしくない。1極回とか行ってほしい。
Hello World!を出力した回数でスキルアップするらしいんだから、極めたい。

多倍長整数

調べていたところ、多倍長整数というものがあるようだ。
Boostライブラリの中あり、メモリに余裕がある限り桁数が実質無制限となるそう。
Boostというのは、c++標準化委員会の委員により設立され、現在でも多くが構成員として残っており、
c++標準化において大きな影響力を持つとされるコミュニティだ(出典)。
インストール方法は割愛させていただく。
Boostライブラリを用いた際のCmakeLists.txtにはこのように追記する。

//オプションの類。ON/OFFは自由。
set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON)

find_package(Boost REQUIRED)
#追加。他のものと一緒にinclude_directoriesに入れる。
include_directories(${Boost_INCLUDE_DIRS})
#追加。他のものと一緒に入れる
target_link_libraries(${Boost_LIBRARIES})

名前空間boostの中のmultiprecisionの中に多倍長整数や任意精度の浮動小数点数などがいろいろ入っている。
整数も浮動小数点数も桁数が実質無制限のクラスがあり、巨大な値や精密な値を扱う際に便利である。

たださすがにメモリがもったいないので有限のものにしようと思う。
無限でもいいのだが、ただでさえバッファリングがメモリを食ってるので、節約できるときはなるべく節約したい。
あらかじめ定義されている有限のもので最大の値を保持できそうなものはuint1024_tだろう。
今回は回数を保持する変数なのでマイナスの値はいらない。正の値だけで1024bitもあるのは狂暴である。
最大値は2^1024-1である。ピンとこない。マジで全くわからん。

日本語の万とか億とか、単位をつけてくれるサイトを探し、変換してみると
17976931348623159077293051907890247336179769789423065727343008115773267580550096313270847732240753602112011387987139335765878976881441662249284743063947412437776789342486548527630221960124609411945308295208500576883815068234246288147391311050無量大数4082不可思議7237那由他1633阿僧祇5051恒河沙684極5862載9823正9947澗2459溝3847穣9716秭3048垓3535京6329兆6242億2413万7215
と、奇怪なことになった。余計わからなくなった。
これがオーバーフローするこのにはHello World!1回あたりのスキルアップがいくら小さくてもかなりスキルアップできることかと思う。
これを使って出力した回数を保存しようと思う。

namespace mp = boost::multiprecision;
int main() {
    //1GiBのバッファリングの設定
    //メモリの確保に失敗するとソフトを終了する。
    if (setvbuf(stdout, NULL, _IOFBF, 1 * 1024 * 1024 * 1024)) {
        printf("failed setvbuf()\n");
        system("pause");
        return 0;
    }
    mp::uint1024_t doing = 0;
    //最大値の設定。unsigned型では0 - 1を行うことで最大値を取得できる。コンパイルで狂うこともなさそう。
    const mp::uint1024_t Uint1024Max = mp::uint1024_t(0) - 1;
    while (true) {
        printf("Hello World!");
        ++doing;
        if (doing == 100'000'000) {
            FILE* fp;
            if (fopen_s(&fp, "Result.txt", "w+") == 0) {
                //uint1024_tはメンバ関数str()でstring型に変換できる。
                //その他multiprecisionでは大体の数値型で変換できる。
                fprintf(fp, "%s", doing.str().c_str());
                fclose(fp);
            }
        }
        if (doing == Uint1024Max) {
            printf("%s", doing.str().c_str());
            break;
        }
    }
    fflush(stdout);
    system("pause > nul");
    return 0;
}

1億回に一回確認用に回数をファイルに出力し、
2^1024 - 1回出力されたら満足なのでそこで出力を終了することにした。
とりあえずしばらく起動しておこうと思う。

~~~~~~~~~~~~~~~~

大体1日くらい放置した。ゲームをやったりしたので最善の記録ではないだろうが、出力された回数は364億回となった。
正直これがいいのか悪いのかわからん。

計測

実際この方法で3分あたり何回出力されるのか計測してみる。
計測用に書き換えたものがこう。

#include <thread>
#include <chrono>
#include <boost/multiprecision/cpp_int.hpp>
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <stdio.h>
#include <windows.h>
#include <sys/stat.h>

using namespace std::chrono;
using hr = high_resolution_clock;
namespace mp = boost::multiprecision;
mp::uint1024_t doing = 0;
const mp::uint1024_t Uint1024Max = mp::uint1024_t(0) - 1;

inline int func() {
    if (setvbuf(stdout, NULL, _IOFBF, 1 * 1024 * 1024 * 1024)) {
        printf("failed setvbuf()\n");
        system("pause");
        return 0;
    }
    while (true) {
        printf("Hello World!");
        ++doing;
    }

    return 0;
}
int main() {
    //sectionプラグマごとに複数のスレッドを用意するプラグマ。
    //num_threadsで用意するスレッド数を指定できる。今回は2つ。
    #pragma omp parallel sections num_threads(2)
    {
        #pragma omp section
            func();
        #pragma omp section
        {
            const auto ep = system_clock::now();
            while (true) {
                if (duration_cast<minutes>(system_clock::now() - ep) >= minutes(3)) {
                    FILE* fp;
                    while (true) {
                        if (fopen_s(&fp, "Result.txt", "w+") == 0) {
                            fprintf(fp, "%s", doing.str().c_str());
                            fclose(fp);
                            break;
                        }
                    }
                    exit(0);
                }
                //現在のスレッドを引数で渡された時間だけ止める。今回の場合は1秒である。
                std::this_thread::sleep_for(seconds(1));
            }
        }
    }
}

OpenMPのsectionsを用いて、スレッドを2つ用意し、
片方では出力を、片方では時間の計測を行う。
doing変数をグローバル変数にして、3分経った際にdoingの値をファイルに書き込み、exit(0)で終了させる。
別のスレッドで回数をファイルに書き出すので、func()関数内での回数書き出し部分は省くことにした。
結果は91,052,458(9105万2458)回。

並列化再び

なぜか遅くなってしまい断念した並列化だが、ソフトをいくつも起動するという方法で強行しようと思う。

カウントはひとつのファイルに書き込むことで共有する。
ただ、少し簡単ではないことがある。
ひとつのソフトが、例えばResult.txtというファイルにデータを書き込んでいるとき、Result.txtは開くことも書き込むこともできなくなるのだ。
書き込むタイミングが被ると、どちらかのソフトのfopen_s関数は失敗することになる。
それでは困るのでResult.txtへの書き出しを行うときはWriting.txtという空のファイルを作成し、その有無で処理を変える。
Result.txtへ書き出しが終了した後、Writing.txtは削除する。
Writing.txtが存在した場合、そのスレッドを一瞬止め、再度存在するかどうか調べる処理を繰り返す。

また、ファイルに回数を出力する部分はHello World!を出力するスレッドとは別のスレッドで実行することにする。

const mp::uint1024_t Uint1024Max = mp::uint1024_t(0) - 1;
unsigned int doing = 0;

void writeCount() {
    //stat用のバッファ
    struct stat statBuf;
    while(true) {
        //Writing.txtが存在するかどうか。しなければwhileを抜ける。
        if (stat("Writing.txt", &statBuf) != 0) break;
        //存在する場合100ミリ秒スレッドを止め、最初のifに戻る。
        std::this_thread::sleep_for(milliseconds(100));
    }
    FILE* writing;
    while(true) {
        //Writing.txtを作成する。失敗すればwhileループでもう一度作成を試みる。
        if (fopen_s(&writing, "Writing.txt", "w+") == 0) {
            fclose(writing);
            break;
        }
    }
    FILE* fp_r;
    //c++らしくnewとするべきなのだろうがmallocのほうが速いので。
    char* readBuf =  reinterpret_cast<char*>(malloc(sizeof(char) * 350));
    if (fopen_s(&fp_r, "Result.txt", "r") == 0) {
        //readBufにファイルの頭から350文字を読み取る。
        fgets(readBuf, 350, fp_r);
        //すぐにクローズする。
        fclose(fp_r);
        //ファイル内の値は多数のソフトで出力された回数の合計であるため、
        //mp::uint1024_tを用いてオーバーフローがないようにする。
        mp::uint1024_t allcount(readBuf);
        //使い終わったらすぐにfreeする。350byteは積もればすぐに山になる。
        free(readBuf);
        //uint1024_t最大値 - 合計出力回数が今回加算しようとしている値を下回れば、
        //uint1024_tの最大値回以上実行できているのでソフトを終了する。
        if (Uint1024Max - allcount < doing) exit(0);
        //加算する
        allcount += doing;
        //doingを0に戻す。unsigned intが溢れないように。
        doing = 0;
        FILE* fp_w;
        if (fopen_s(&fp_w, "Result.txt", "w") == 0) {
            fprintf_s(fp_w, "%s", allcount.str().c_str());
            fclose(fp_w);
        }
    }
    //Writing.txtを削除する。
    DeleteFileA("Writing.txt");
}

inline int func() {
    if (setvbuf(stdout, NULL, _IOFBF, 1 * 1024 * 1024 * 1024)) {
        printf("failed setvbuf()\n");
        system("pause");
        return 0;
    }

    struct stat statBuf;
    if (stat("Result.txt", &statBuf) != 0) {
        FILE* fp;
        fopen_s(&fp, "Result.txt", "w+");
        fprintf_s(fp, "0");
        fclose(fp);
    }

    //std::threadクラスの変数を書き換える際は前に入っていた関数がjoinされている必要がある。
    //そのため、最初にjoinが呼び出された際、エラーにならないよう、簡単な関数を入れておく。
    //returnだけでよいのでラムダ式で。
    std::thread writeFunc([]{return;});

    while (true) {
        printf("Hello World!");
        ++doing;
        if (doing == 1'000'000) {
            //書き換える際はjoinされている必要があるのでjoinする。
            //なるべくjoinする必要のないようにifで剰余を求める際の右オペランドの値は大きめにする。
            writeFunc.join();
            writeFunc = std::thread(writeCount);
        }
    }

    return 0;
}

int main() {
    #pragma omp parallel sections num_threads(2)
    {
        #pragma omp section
            func();
        #pragma omp section
        {
            const auto ep = system_clock::now();
            while (true) {
                if (duration_cast<minutes>(system_clock::now() - ep) >= minutes(3)) {
                    exit(0);
                }
                std::this_thread::sleep_for(seconds(1));
            }
        }
    }
    return 0;
}

ソースコードもかなり長くなってきたなぁ。

計測のためソフトを1度に1つだけ起動してみる。
Result.txtに書かれていた出力回数は106,158,884(1億615万8884)回。

doingをグローバル変数にしたので、writeFuncスレッドがWriting.txtが存在しないかなどの調査中にも回数が書き換わり、端数も加算できるようになった。ローカル変数にしてスレッドにコピーして渡すのでは得られない効果だろう。
書き始めた際はdoingをローカル変数にし、参照渡ししてみたりもしたのだが、グローバル変数のほうが速かった。

だが、なぜか3分でソフトが終了せず、14秒ほどオーバーした。
14秒というのは当方が違和感を持ったためスマートフォンのタイマーとアプリの起動をある程度同時に行い計測したもの。

exitによる後処理の結果だと思うが、その14秒の間にも出力が行われ、
doingが変わっていたとすれば、計測に誤差が出るので、後処理を行わない終了関数を探す。
_exitという関数は後処理を行わないとのこと。ストリームのフラッシュは処理系依存らしい。

スマホのタイマーと同時に行い計測したところ3分ちょうどで処理が終了した。
g++では_exit関数はストリームをフラッシュしないらしい。
出力された回数は114,170,401(1億1417万401)回。増えた。

こそこそ噂話

誤差がえぐい。実行するたびに1000万くらいの誤差が出る。
制限時間を1分にすると1の誤差も出ないことがあるほど誤差が出ないのに3分にすると急にこのざま。
どこからが境界線かも対処のしようもわからないのでこのままにしておくけど不安しかない。

マルチスレッドに挑戦

現在の処理では出力に1スレッド、ファイルに書き出すのに1スレッド、タイマーに1スレッド使っているので合計3スレッド使用している。
当方が現在使用しているCPUは16スレッドであるため、むやみに16個も同時に起動するより5つに抑えておき、
使用するスレッドが被らないほうが無難だと思ったからである。

手動で起動してもいいんだけど今回はこういうバッチを書く

起動用バッチ
@echo off
for /l %%n in (1,1,5) do (
    start HelloWorld
)

startは呼び出したアプリの終了を待たず、次の処理を行うもの。これで自らクリックするより多少速く起動できる。

合計出力回数は452,401,350(4億5240万1350)回。
1つしか起動していないときと比べると約4.999倍。

これで12時間起動してみようと思う。おやすみなさい。

おはようございます。
Relust.txtに書かれていた数字は9,020,177,898(90億2017万7898)でした。20倍ほどです。やったね。

おかしくない...?
試しに1分で終了するように書き換えて実行してみると447,392,425(4億4739万2425)。
大体20倍しか差がない。12時間は720分だから720倍にならなきゃおかしいのだ。7を勝手に取らないでいただきたい。

何度もテストしているうちにわかったことがある。

Result.txtに出る数字、決まってるわ......

何を言ってるかわからないと思うが当方も全くわからん。447392425って数字もうめっちゃみた。すげぇでる。制限時間3分にしてもこれなんだよ。なんでだよ。ファイルに出力する関数はfopen_sが成功するまでwhileループが抜けられないはずだしもうなんなんだよ。

おわりたい。
おわります。

おわり。

const mp::uint1024_t Uint1024Max = mp::uint1024_t(0) - 1;
unsigned long int doing = 0;

void writeCount() {
    struct stat statBuf;
    while(true) {
        if (stat("Writing.txt", &statBuf) != 0) break;
        std::this_thread::sleep_for(milliseconds(3));
    }
    FILE* writing;
    while(true) {
        if (fopen_s(&writing, "Writing.txt", "w") == 0) {
            fclose(writing);
            break;
        }
    }
    FILE* fp_r;
    char* readBuf =  reinterpret_cast<char*>(malloc(sizeof(char) * 350));
    while (true) {
        if (fopen_s(&fp_r, "Result.txt", "r") == 0) {
            fgets(readBuf, 350, fp_r);
            fclose(fp_r);
            mp::uint1024_t allcount(readBuf);
            free(readBuf);
            if (Uint1024Max - allcount < doing) {
                FILE* tmp;
                fopen_s(&tmp, "Complete.txt", "w");
                fclose(tmp);
                _exit(0);
            }
            allcount += doing;
            doing = 0;
            FILE* fp_w;
            while (true) {
                if (fopen_s(&fp_w, "Result.txt", "w") == 0) {
                    fprintf_s(fp_w, "%s", allcount.str().c_str());
                    fclose(fp_w);
                    break;
                } else OutputDebugStringA("Failed fopen_s(\"Result.txt\", \"w\")");
            }
            break;
        } else OutputDebugStringA("Failed fopen_s(\"Result.txt\", \"r\")");
    }
    DeleteFileA("Writing.txt");
}

inline int func() {
    if (setvbuf(stdout, NULL, _IOFBF, 1 * 1024 * 1024 * 1024)) {
        printf("failed setvbuf()\n");
        system("pause");
        return 0;
    }

    struct stat statBuf;
    if (stat("Result.txt", &statBuf) != 0) {
        FILE* fp;
        fopen_s(&fp, "Result.txt", "w");
        fprintf_s(fp, "0");
        fclose(fp);
    }

    std::thread writeFunc([]{return;});

    while (true) {
        printf("Hello World!");
        ++doing;
        if (doing == 1'000'000'000'000ul) {
            writeFunc.join();
            writeFunc = std::thread(writeCount);
        }
    }

    return 0;
}

int main() {
    func();
    return 0;
}

最終的なソースコード。制限時間とかを適当に取っ払っただけ。funcはインラインにしてるけど内容を直接mainに書いたほうがいいと思います。計測用のコードから書き換えるのが面倒なのでしませんが。

おしまい

これだけ長々と書いておいて終わり方が急すぎて申し訳ない。飽きてしまったので許してほしい。

最初に紹介したブログに書いてあったことが面白かったので、それをもとにHello World!を何度も大量に出力しながら、スキルアップを狙う記事を目指していたのだが、プログラムの速度を意識するあまりc++というよりほぼcみたいなコードになってしまった。

まぁc++を趣味で始めたころ「GUIを作るにはどうすればいいんだろう」とか「なんでCPUのコアを十分に使えないんだろう」とか「巨大な数値を扱うときはどうすればいいんだろう」とか思っていたので、そのあたりのライブラリを紹介出来て満足です。
GUIを作るにはQtが便利。マルチスレッドを扱う際はOpenMPかTBBあたりが便利。巨大な数値を扱うときはBoostライブラリのmultiprecisionが便利。

結論

Hello Worldを数億回程度出力したくらいじゃプログラミングスキルは伸びない。

最後の問題を解決出来たら続きます。続かないと思います。

345
165
9

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
345
165