web-dev-qa-db-ja.com

以前に割り当てられたベクターのPush_backがoperator []より遅い理由

私はこのブログを読んだだけです http://lemire.me/blog/archives/2012/06/20/do-not-waste-time-with-stl-vectors/operator[]割り当てとPush_backのメモリは事前予約済みstd::vectorで、私は自分で試すことにしました。操作は簡単です:

// for vector
bigarray.reserve(N);

// START TIME TRACK
for(int k = 0; k < N; ++k)
   // for operator[]:
   // bigarray[k] = k;
   // for Push_back
   bigarray.Push_back(k);
// END TIME TRACK

// do some dummy operations to prevent compiler optimize
long sum = accumulate(begin(bigarray), end(array),0 0);

そしてここに結果があります:

 ~/t/benchmark> icc 1.cpp -O3 -std=c++11
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.789123s  --> C++ new
[               1.cpp:   52]     0.774049s  --> C++ new
[               1.cpp:   66]     0.351176s  --> vector
[               1.cpp:   80]     1.801294s  --> reserve + Push_back
[               1.cpp:   94]     1.753786s  --> reserve + emplace_back
[               1.cpp:  107]     2.815756s  --> no reserve + Push_back
 ~/t/benchmark> clang++ 1.cpp -std=c++11 -O3
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.592318s  --> C++ new
[               1.cpp:   52]     0.566979s  --> C++ new
[               1.cpp:   66]     0.270363s  --> vector
[               1.cpp:   80]     1.763784s  --> reserve + Push_back
[               1.cpp:   94]     1.761879s  --> reserve + emplace_back
[               1.cpp:  107]     2.815596s  --> no reserve + Push_back
 ~/t/benchmark> g++ 1.cpp -O3 -std=c++11
 ~/t/benchmark> ./a.out
[               1.cpp:   52]     0.617995s  --> C++ new
[               1.cpp:   52]     0.601746s  --> C++ new
[               1.cpp:   66]     0.270533s  --> vector
[               1.cpp:   80]     1.766538s  --> reserve + Push_back
[               1.cpp:   94]     1.998792s  --> reserve + emplace_back
[               1.cpp:  107]     2.815617s  --> no reserve + Push_back

すべてのコンパイラーで、vectoroperator[]の組み合わせは、operator[]との未加工ポインターよりもはるかに高速です。これが最初の質問につながりました:なぜですか? 2番目の質問は、私はすでにメモリを「予約」しているのですが、なぜopeator[]の方が速いのですか?

次の質問は、メモリがすでに割り当てられているので、なぜPush_backoperator[]よりも遅いのですか?

テストコードは以下に添付されています:

#include <iostream>
#include <iomanip>
#include <vector>
#include <numeric>
#include <chrono>
#include <string>
#include <cstring>

#define PROFILE(BLOCK, ROUTNAME) ProfilerRun([&](){do {BLOCK;} while(0);}, \
        ROUTNAME, __FILE__, __LINE__);

template <typename T>
void ProfilerRun (T&&  func, const std::string& routine_name = "unknown",
                  const char* file = "unknown", unsigned line = 0)
{
    using std::chrono::duration_cast;
    using std::chrono::microseconds;
    using std::chrono::steady_clock;
    using std::cerr;
    using std::endl;

    steady_clock::time_point t_begin = steady_clock::now();

    // Call the function
    func();

    steady_clock::time_point t_end = steady_clock::now();
    cerr << "[" << std::setw (20)
         << (std::strrchr (file, '/') ?
             std::strrchr (file, '/') + 1 : file)
         << ":" << std::setw (5) << line << "]   "
         << std::setw (10) << std::setprecision (6) << std::fixed
         << static_cast<float> (duration_cast<microseconds>
                                (t_end - t_begin).count()) / 1e6
         << "s  --> " << routine_name << endl;

    cerr.unsetf (std::ios_base::floatfield);
}

using namespace std;

const int N = (1 << 29);

int routine1()
{
    int sum;
    int* bigarray = new int[N];
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "C++ new");
    sum = std::accumulate (bigarray, bigarray + N, 0);
    delete [] bigarray;
    return sum;
}

int routine2()
{
    int sum;
    vector<int> bigarray (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "vector");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine3()
{
    int sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "reserve + Push_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine4()
{
    int sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.emplace_back(k);
    }, "reserve + emplace_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

int routine5()
{
    int sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "no reserve + Push_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}


int main()
{
    long s0 = routine1();
    long s1 = routine1();
    long s2 = routine2();
    long s3 = routine3();
    long s4 = routine4();
    long s5 = routine5();
    return int (s1 + s2);
}
35
xis

Push_backは境界チェックを行います。 operator[] ではない。したがって、スペースを予約した場合でも、Push_backoperator[] ありません。さらに、size値を増加させます(予約はcapacityを設定するだけです)。そのため、毎回それを更新します。

要するに、 Push_backは何をしているのかoperator[]が実行中-これが遅い(そしてより正確な)理由です。

46
Zac Howland

Yakkと私が発見したように、Push_backの明らかな遅延の原因となっている別の興味深い要因があるかもしれません。

最初の興味深い観察は、元のテストでnewを使用して生の配列を操作することは、vector<int> bigarray(N);operator[]を使用するよりもslowerであり、因数2よりも多いことです。 。さらに興味深いのは、生の配列バリアントにadditionalmemsetを挿入することにより、両方で同じパフォーマンスが得られることです。

int routine1_modified()
{
    int sum;
    int* bigarray = new int[N];

    memset(bigarray, 0, sizeof(int)*N);

    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "C++ new");
    sum = std::accumulate (bigarray, bigarray + N, 0);
    delete [] bigarray;
    return sum;
}

もちろん、結論は、PROFILEは予想とは異なる何かを測定するということです。 Yakkと私は、メモリ管理と関係があると思います。 YakkのOPへのコメントから:

resizeは、メモリのブロック全体にアクセスします。 reserveは触れずに割り当てます。アクセスするまで物理メモリページをフェッチまたは割り当てないレイジーアロケーターがある場合、空のベクターのreserveはほとんど解放されます(ページの物理メモリを見つける必要すらありません!)ページに書き込みます(その時点で、それらを見つける必要があります)。

私は似たようなものを考えたので、「strided memset」で特定のページをタッチして、この仮説の簡単なテストを試みました(プロファイリングツールはより信頼性の高い結果を得る可能性があります)。

int routine1_modified2()
{
    int sum;
    int* bigarray = new int[N];

    for(int k = 0; k < N; k += PAGESIZE*2/sizeof(int))
        bigarray[k] = 0;

    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "C++ new");
    sum = std::accumulate (bigarray, bigarray + N, 0);
    delete [] bigarray;
    return sum;
}

ストライドをすべてのページの半分から4ページごとに変更して完全に省略することにより、vector<int> bigarray(N);ケースからタイミングへの素敵な移行が得られますmemsetが使用されていないnew int[N]のケース。

私の意見では、これはメモリ管理が測定結果の主な原因であることを強く示唆しています。


別の問題は、Push_backでの分岐です。これがPush_backを使用する場合と比較してoperator[]much遅い主な理由であると多くの回答で主張されています。実際、memsetなしのrawポインターをreserve + Push_backを使用する場合と比較すると、前者の方が2倍高速です。

同様に、UBを少し追加した場合(ただし、結果を後で確認します):

int routine3_modified()
{
    int sum;
    vector<int> bigarray;
    bigarray.reserve (N);

    memset(bigarray.data(), 0, sizeof(int)*N); // technically, it's UB

    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "reserve + Push_back");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

この変更されたバージョンは、new +フルmemsetを使用する場合よりも約2倍遅くなります。したがって、Push_backの呼び出しが何をするようにも見え、要素を設定するだけの場合と比較すると、2の要素が(vectorとraw配列の両方でoperator[]を介して)スローダウンします。

しかし、それはPush_backで必要な分岐ですか、それとも追加の操作ですか?

// pseudo-code
void Push_back(T const& p)
{
    if(size() == capacity())
    {
        resize( size() < 10 ? 10 : size()*2 );
    }

    (*this)[size()] = p; // actually using the allocator
    ++m_end;
}

確かにそれは簡単です。 libstdc ++の実装

vector<int> bigarray(N); + operator[]バリアントを使用してテストし、Push_backの動作を模倣する関数呼び出しを挿入しました。

unsigned x = 0;
void silly_branch(int k)
{
    if(k == x)
    {
        x = x < 10 ? 10 : x*2;
    }
}

int routine2_modified()
{
    int sum;
    vector<int> bigarray (N);
    PROFILE (
    {
        for (unsigned int k = 0; k < N; ++k)
        {
            silly_branch(k);
            bigarray[k] = k;
        }
    }, "vector");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0);
    return sum;
}

xを揮発性として宣言した場合でも、これは測定に1%の影響しか与えません。もちろん、ブランチが実際にopcodeであることを確認する必要がありましたが、私のアセンブラーの知識では、(-O3で)それを確認できません。

ここで興味深い点は、silly_branchにインクリメントを追加するとどうなるかです。

unsigned x = 0;
void silly_branch(int k)
{
    if(k == x)
    {
        x = x < 10 ? 10 : x*2;
    }
    ++x;
}

現在、変更されたroutine2_modifiedは、元のroutine2の2倍の速度で実行され、メモリページをコミットするUBを含む上記のroutine3_modifiedと同等です。ループ内のすべての書き込みに別の書き込みが追加されるため、これは特に驚くべきことではないので、2倍の作業と2倍の期間があります。


結論

メモリ管理の仮説を検証するために、アセンブリツールとプロファイリングツールを注意深く確認する必要があり、追加の書き込みは適切な仮説です(「正しい」)。しかし、ヒントは、Push_backを遅くする単なるブランチよりも複雑なことが起こっていると主張するのに十分強力だと思います。

これが完全なテストコードです:

#include <iostream>
#include <iomanip>
#include <vector>
#include <numeric>
#include <chrono>
#include <string>
#include <cstring>

#define PROFILE(BLOCK, ROUTNAME) ProfilerRun([&](){do {BLOCK;} while(0);}, \
        ROUTNAME, __FILE__, __LINE__);
//#define PROFILE(BLOCK, ROUTNAME) BLOCK

template <typename T>
void ProfilerRun (T&&  func, const std::string& routine_name = "unknown",
                  const char* file = "unknown", unsigned line = 0)
{
    using std::chrono::duration_cast;
    using std::chrono::microseconds;
    using std::chrono::steady_clock;
    using std::cerr;
    using std::endl;

    steady_clock::time_point t_begin = steady_clock::now();

    // Call the function
    func();

    steady_clock::time_point t_end = steady_clock::now();
    cerr << "[" << std::setw (20)
         << (std::strrchr (file, '/') ?
             std::strrchr (file, '/') + 1 : file)
         << ":" << std::setw (5) << line << "]   "
         << std::setw (10) << std::setprecision (6) << std::fixed
         << static_cast<float> (duration_cast<microseconds>
                                (t_end - t_begin).count()) / 1e6
         << "s  --> " << routine_name << endl;

    cerr.unsetf (std::ios_base::floatfield);
}

using namespace std;

constexpr int N = (1 << 28);
constexpr int PAGESIZE = 4096;

uint64_t __attribute__((noinline)) routine1()
{
    uint64_t sum;
    int* bigarray = new int[N];
    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new (routine1)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine2()
{
    uint64_t sum;
    int* bigarray = new int[N];

    memset(bigarray, 0, sizeof(int)*N);

    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new + full memset (routine2)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine3()
{
    uint64_t sum;
    int* bigarray = new int[N];

    for(int k = 0; k < N; k += PAGESIZE/2/sizeof(int))
        bigarray[k] = 0;

    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new + strided memset (every page half) (routine3)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine4()
{
    uint64_t sum;
    int* bigarray = new int[N];

    for(int k = 0; k < N; k += PAGESIZE/1/sizeof(int))
        bigarray[k] = 0;

    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new + strided memset (every page) (routine4)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine5()
{
    uint64_t sum;
    int* bigarray = new int[N];

    for(int k = 0; k < N; k += PAGESIZE*2/sizeof(int))
        bigarray[k] = 0;

    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new + strided memset (every other page) (routine5)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine6()
{
    uint64_t sum;
    int* bigarray = new int[N];

    for(int k = 0; k < N; k += PAGESIZE*4/sizeof(int))
        bigarray[k] = 0;

    PROFILE (
    {
        for (int k = 0, *p = bigarray; p != bigarray+N; ++p, ++k)
            *p = k;
    }, "new + strided memset (every 4th page) (routine6)");
    sum = std::accumulate (bigarray, bigarray + N, 0ULL);
    delete [] bigarray;
    return sum;
}

uint64_t __attribute__((noinline)) routine7()
{
    uint64_t sum;
    vector<int> bigarray (N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray[k] = k;
    }, "vector, using ctor to initialize (routine7)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine8()
{
    uint64_t sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "vector (+ no reserve) + Push_back (routine8)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine9()
{
    uint64_t sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "vector + reserve + Push_back (routine9)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine10()
{
    uint64_t sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    memset(bigarray.data(), 0, sizeof(int)*N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.Push_back (k);
    }, "vector + reserve + memset (UB) + Push_back (routine10)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

template<class T>
void __attribute__((noinline)) adjust_size(std::vector<T>& v, int k, double factor)
{
    if(k >= v.size())
    {
        v.resize(v.size() < 10 ? 10 : k*factor);
    }
}

uint64_t __attribute__((noinline)) routine11()
{
    uint64_t sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
        {
            adjust_size(bigarray, k, 1.5);
            bigarray[k] = k;
        }
    }, "vector + custom emplace_back @ factor 1.5 (routine11)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine12()
{
    uint64_t sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
        {
            adjust_size(bigarray, k, 2);
            bigarray[k] = k;
        }
    }, "vector + custom emplace_back @ factor 2 (routine12)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine13()
{
    uint64_t sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
        {
            adjust_size(bigarray, k, 3);
            bigarray[k] = k;
        }
    }, "vector + custom emplace_back @ factor 3 (routine13)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine14()
{
    uint64_t sum;
    vector<int> bigarray;
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.emplace_back (k);
    }, "vector (+ no reserve) + emplace_back (routine14)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine15()
{
    uint64_t sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.emplace_back (k);
    }, "vector + reserve + emplace_back (routine15)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

uint64_t __attribute__((noinline)) routine16()
{
    uint64_t sum;
    vector<int> bigarray;
    bigarray.reserve (N);
    memset(bigarray.data(), 0, sizeof(bigarray[0])*N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
            bigarray.emplace_back (k);
    }, "vector + reserve + memset (UB) + emplace_back (routine16)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

unsigned x = 0;
template<class T>
void /*__attribute__((noinline))*/ silly_branch(std::vector<T>& v, int k)
{
    if(k == x)
    {
        x = x < 10 ? 10 : x*2;
    }
    //++x;
}

uint64_t __attribute__((noinline)) routine17()
{
    uint64_t sum;
    vector<int> bigarray(N);
    PROFILE (
    {
        for (int k = 0; k < N; ++k)
        {
            silly_branch(bigarray, k);
            bigarray[k] = k;
        }
    }, "vector, using ctor to initialize + silly branch (routine17)");
    sum = std::accumulate (begin (bigarray), end (bigarray), 0ULL);
    return sum;
}

template<class T, int N>
constexpr int get_extent(T(&)[N])
{  return N;  }

int main()
{
    uint64_t results[] = {routine2(),
    routine1(),
    routine2(),
    routine3(),
    routine4(),
    routine5(),
    routine6(),
    routine7(),
    routine8(),
    routine9(),
    routine10(),
    routine11(),
    routine12(),
    routine13(),
    routine14(),
    routine15(),
    routine16(),
    routine17()};

    std::cout << std::boolalpha;
    for(int i = 1; i < get_extent(results); ++i)
    {
        std::cout << i << ": " << (results[0] == results[i]) << "\n";
    }
    std::cout << x << "\n";
}

古い低速のコンピューターでのサンプル実行。注意:

  • OPのようにN == 2<<28ではなく2<<29
  • -std=c++11 -O3 -march=nativeを使用してg ++ 4.9 20131022でコンパイル
 [temp.cpp:71] 0.654927s-> new + full memset(routine2)
 [temp.cpp:54] 1.042405s-> new(routine1)
 [temp.cpp:71] 0.605061s-> new + full memset(routine2)
 [temp.cpp:89] 0.597487s-> new + strided memset(every page half)(routine3)
 [temp.cpp:107] 0.601271s-> new + strided memset(every page)(routine4)
 [temp.cpp:125] 0.783610s-> new + strided memset(その他すべて)ページ)(routine5)
 [temp.cpp:143] 0.903038s-> new + strided memset(4ページごと)(routine6)
 [temp.cpp:157] 0.602401s- >ベクトル、初期化にctorを使用(routine7)
 [temp.cpp:170] 3.811291s->ベクトル(+予約なし)+ Push_back(routine8)
 [temp.cpp:184] 2.091391s->ベクトル+予約+ Push_back(routine9)
 [temp.cpp:199] 1.375837s- -> vector + reserve + memset(UB)+ Push_back(routine10)
 [temp.cpp:224] 8.738293s-> vector + custom emplace_back @ factor 1.5(routine11)
 [temp。 cpp:240] 5.513803s-> vector + custom emplace_back @ factor 2(routine12)
 [temp.cpp:256] 5.150388s-> vector + custom emplace_back @ factor 3(routine13)
 [temp.cpp:269] 3.789820s->ベクトル(+予約なし)+ emplace_back(routine14)
 [temp.cpp:283] 2.090259s->ベクトル+予約+ emplace_back(routine15)
 [temp.cpp:298] 1.288740s->ベクトル+予約+ memset(UB)+ emplace_back(routine16)
 [temp.cpp:325] 0.611168s->ベクトル、ctorを使用初期化+愚かなブランチ(ルーチン17)
 1:true 
 2:true 
 3:true 
 4:true 
 5:true 
 6:真
 7:真
 8:真
 9:真
 10:真
 11:真
 12:真
 13:真
 14:真
 15:真
 16:真
 17:真
 335544320 
28
dyp

コンストラクターで配列を割り当てると、コンパイラー/ライブラリーは基本的に元のフィルをmemset()してから、個々の値を設定できます。 Push_back()を使用すると、std::vector<T>クラスは次のことを行う必要があります。

  1. 十分なスペースがあるか確認してください。
  2. 終了ポインタを新しい場所に変更します。
  3. 実際の値を設定します。

最後のステップは、メモリが一度に割り当てられるときに実行する必要がある唯一のことです。

8
Dietmar Kühl

2つ目の質問にお答えします。ベクトルは事前に割り当てられていますが、Push_backは、Push_backを呼び出すたびに使用可能なスペースを確認する必要があります。一方、operator []はチェックを実行せず、スペースが利用可能であると想定します。

3
Jasper

これは回答ではなく拡張コメントであり、質問の改善に役立ちます。

ルーチン4は未定義の動作を呼び出します。配列のsizeの終わりを超えて書き込んでいます。リザーブをサイズ変更に置き換えて、それを排除します。

ルーチン3から5では、監視可能な出力がないため、最適化後は何もできません。

insert( vec.end(), src.begin(), src.end() )ここで、srcはランダムアクセスジェネレーターの範囲です(boostおそらく持っている)newの場合、insertバージョンをエミュレートできますスマートです。

routine1の複製はおかしいようです-たぶん、これによりタイミングが変わりますか?