4

私は大学のクラスにプログラムを書いています。これは、2 つのプロセッサでタスクをスケジューリングするための単純なバージョンの動的計画法アルゴリズムの実装です。メモリの無駄使いなので、改善策を考えてみました。たとえば、S xn 長方形配列全体を格納する必要はありません。ここで、S はすべてのタスクの時間の合計であり、n はタスクの数です。アルゴリズムの最初の反復では、データは n 軸の小さなインデックス値にのみ格納されるため、配列を三角形にすることができると考えました。つまり、次の各サブ配列は特定の量の要素が長くなります。

次に、タスク マネージャーでメモリ使用量を調べたところ、ショックを受けました。長方形配列のバージョンは 980KB かかりました。トライアングル アレイを使用したバージョン (小さい方のアレイ) は、ほぼ 15 MB かかりました。システムで使用されるメモリ割り当ての方法について何も知らないか、妄想を持っているのかもしれません。または、コードで愚かな間違いを犯しました。でもきっと何かわからない。誰かが私を啓発できますか?

これが私のコードです:

#include <iostream>
#include <fstream> 
#include <conio.h>
#include <stack>

using namespace std;

void readTasks(char* filename, int*& outTaskArray, int& outTaskCount)
{
    ifstream input = ifstream();
    input.open(filename, ios::in);

    input >> outTaskCount;
    outTaskArray = new int[outTaskCount];
    for (int i = 0; i < outTaskCount; ++i)
    {
        input >> outTaskArray[i];
    }

    input.close();
}

void coutTasks(int* taskArray, int taskCount)
{
    cout << taskCount << " tasks:\n";
    for (int i = 0; i < taskCount; ++i)
    {
        cout << i << ": " << taskArray[i] << endl;
    }
}

void Scheduling2(int* taskArray, int taskCount, int memorySaving, 
    stack<int>*& outP1Stack, stack<int>*& outP2Stack)
{
    bool** scheduleArray = new bool*[taskCount];
    int sum;

    // I know that construction below is ugly cause of code repetition.
    // But I'm rather looking for performance, so I try to avoid e.g. 
    // checking the same condition too many times.
    if (memorySaving == 0)
    {   
        sum = 0;
        for (int i = 0; i < taskCount; ++i)
        {
            sum += taskArray[i];
        }

        scheduleArray[0] = new bool[sum + 1];
        for (int j = 0; j < sum + 1; ++j)
        {
            scheduleArray[0][j] = j == 0 || j == taskArray[0];
        }
        for (int i = 1; i < taskCount; ++i)
        {
            scheduleArray[i] = new bool[sum + 1];
            for (int j = 0; j < sum + 1; ++j)
            {
                scheduleArray[i][j] = scheduleArray[i - 1][j] || 
                    j >=  taskArray[i] && 
                    scheduleArray[i - 1][j - taskArray[i]];
            }
        }

        getch(); // I'm reading memory usage from Task Manager when program stops here

        int halfSum = sum >> 1;
        while (!scheduleArray[taskCount - 1][halfSum]) --halfSum;

        for (int i = taskCount - 1; i > 0; --i)
        {
            if (scheduleArray[i - 1][halfSum])
                outP1Stack->push(i);
            else if (scheduleArray[i - 1][halfSum - taskArray[i]])
            {
                outP2Stack->push(i);
                halfSum -= taskArray[i];
            }
        }
        if (halfSum) outP2Stack->push(0);
            else outP1Stack->push(0);
    }
    else if (memorySaving == 1)
    {   
        sum = 0;
        for (int i = 0; i < taskCount; ++i)
        {
            sum += taskArray[i];

            scheduleArray[0] = new bool[sum + 1];
            for (int j = 0; j < sum + 1; ++j)
            {
                scheduleArray[0][j] = j == 0 || j == taskArray[0];
            }
            for (int i = 1; i < taskCount; ++i)
            {
                scheduleArray[i] = new bool[sum + 1];
                        for (int j = 0; j < sum + 1; ++j)
                {
                    scheduleArray[i][j] = scheduleArray[i - 1][j] || 
                        j >= taskArray[i] && 
                        scheduleArray[i - 1][j - taskArray[i]];
                }
            }
        }

        getch(); // I'm reading memory usage from Task Manager when program stops here

        int halfSum = sum >> 1;
        while (!scheduleArray[taskCount - 1][halfSum]) --halfSum;

        for (int i = taskCount - 1; i > 0; --i)
        {
            if (scheduleArray[i - 1][halfSum])
                outP1Stack->push(i);
            else if (scheduleArray[i - 1][halfSum - taskArray[i]])
            {
                outP2Stack->push(i);
                halfSum -= taskArray[i];
            }
        }
        if (halfSum) outP2Stack->push(0);
        else outP1Stack->push(0);
    }

    for (int i = 0; i < taskCount; ++i)
    {
        delete[] scheduleArray[i];
    }
    delete[] scheduleArray;
}

int main()
{
    char* filename = "input2.txt";
    int memorySaving = 0; //changing to 1 in code when testing memory usage

    int* taskArray; // each number in array equals time taken by task
    int taskCount;
    readTasks(filename, taskArray, taskCount);

    coutTasks(taskArray, taskCount);

    stack<int>* p1Stack = new stack<int>();
    stack<int>* p2Stack = new stack<int>();

    Scheduling2(taskArray, taskCount, memorySaving, p1Stack, p2Stack);

    cout << "\np1: ";
    while (p1Stack->size())
    {
        cout << p1Stack->top() << ", ";
        p1Stack->pop();
    }
    cout << "\np2: ";
    while (p2Stack->size())
    {
        cout << p2Stack->top() << ", ";
        p2Stack->pop();
    }

    delete p1Stack;
    delete p2Stack;

    delete[] taskArray;
    return 0;
}
4

2 に答える 2

2

くそー、私は盲目です。私は非常に大きなメモリリークがあり、それを見ませんでした。実行された部分を調べたところmemorySaving == 1、配列時間のすべての行を割り当てていることに気付きました (神はその理由を知っています) taskCount...これを書いているとき、それは私が意図したものではありません。良い。深夜だった。

皆様お騒がせして申し訳ありません。質問はクローズする必要があります。

于 2012-11-29T11:49:14.867 に答える
1

私の提案はあなたの問題を解決していたので(私が疑ったように)、私はそれらを答えにします!

しかし、この場合、なぜベクトルを使用すると役立つのでしょうか? その機能を使用する必要はありません。

はい、そうでした!その最も重要な「機能」の 1 つが必要でした...配列メモリ ブロックの自動管理です。宣言がvector< vector<bool> > scheduleArrayである場合、リークはあり得ないことに注意してください。あなたのコードには new も delete もありませんでした...どうすればそれが可能になるでしょうか?

ベクターを使用するその他の利点:

  • 誤ってポインターのdelete代わりに実行することはできませんdelete[]

  • 境界チェックを行うことができます(有効にした場合は、デバッグ ビルドでこれを行う必要があります...これが有効になってvector<int> v; v[0] = 1;いることを確認するためだけにテストを試みてください)。

  • ベクトルは保持している要素の数を認識しているため、 のようなパラメーターを渡さなければならない状況に遭遇することはありませんtaskCount。これにより、簿記で間違いを犯す可能性のある場所がもう 1 つなくなります。 (たとえば、ベクトルから要素を削除し、それをカウント変数に反映するのを忘れた場合はどうなるでしょうか?)

コメントへの回答:

ビットシフト操作は 2 で割るより高速ではありませんか?

いいえ。

生のアセンブリでコーディングしている場合、特定のアーキテクチャではそうなる場合があります。しかし、ほとんどの場合、整数の除算とビット シフトには、いずれにしても 1 サイクル全体のコストがかかります。そして、シフトよりも速く分割できる奇抜なアーキテクチャがそこにあると確信しています。

これは C++ であり、アセンブリではないことに注意してください。コードを明確に保ち、​​オプティマイザーが正しいことを行うと信頼することが最善です。たとえば、SHIFT と DIV の両方が 1 命令サイクルかかるが、パイプラインに関する何かが原因でタイトなループに陥っている場合に、どちらを使用するかを交互に使用することで速度を上げることができる場合はどうなるでしょうか?

memorySaving は 2 つだけではなく、より多くの値を持つようになります。

次に、列挙型を使用します。

配列のように、std::vector O(1) はインデックスごとにすべての要素へのアクセス時間を持っていますか?

はい、あなたが発見したように。ストレージに関しては、ベクトルごとに少量のオーバーヘッドがあります(コンパイラによって異なります)。しかし、上で述べたように、これは支払うべき小さな代償です。また、おそらく最初は配列外の変数の長さを追跡していたでしょう。

スタックへのポインタについては、メモリをいつ解放するかを自分で決めることができるので、動的メモリ割り当ての方が一般的に優れているのではないでしょうか?

まさにその理由で、一般的には良くありません。メモリをいつ解放するかを自分で決定する責任がある場合は、その責任を管理することにボールを落とすことができます。

したがって、可能な限り、C++ のスコープにオブジェクトの有効期間を処理させる必要があります。作成の範囲外で存続する動的オブジェクトを作成することは、避けられない場合がありますが、それが Modern C++ にスマート ポインターがある理由です。それらを使用してください!

http://en.wikipedia.org/wiki/Smart_pointer

たとえば、readTasksが返された場合、よりクリーンで安全になりますshared_ptr< vector<int> >

サポートされている関数を使用せず、ここで char* が std::string と同じくらい優れている場合、なぜ std::string を使用する必要があるのでしょうか。

上記の vector の引数と同様の理由で、それを使用しない習慣を身につけるため。たとえば、境界チェック。また、クイズの質問: "input2.txt" を大文字にしたいときに と言ったらどうなると思いますfilename[0] = 'I';か?

私の提案をすべて実装したら、 boost::dynamic_bitsetを確認できます。:-)

于 2012-11-29T13:07:33.520 に答える