2016-08-13 8 views
0

私は.reduceメソッドを使用して、特定の条件に最も適したオブジェクトの配列インデックスを返すためにオブジェクトの配列を反復処理します。私の配列には現在約3万のインデックスがあり、私は100万を越えて撮影しています。問題は、.reduceを使用して配列を反復処理することです。現時点では約4秒の話をしていますが、配列に予測される100万のインデックスがあるかどうかを想像してください。アレイはコンパクトです。私はデータベースやサーバーに接続していません。ここに私のコードは次のとおりです。Javascript:パフォーマンスのために `reduce`を最適化する

var startMatchMaking = function() { 
    var loopCounter = 0; 
    var length = personArray.length; 
    do { 
     var manCounter = 0; 
     loopCounter++; 
     for (var i = length; i--;){ 
      if (!personArray[i].isSingle && personArray[i].sex === "Male" && 
       personArray[i].isAvailable === true) { 
       manCounter++;    
       var num = normalRandomScaled(2.1, 12.44); 

       var result = personArray.reduce(function(p,c,k,a){ 
        return c.sex !== personArray[i].sex && 
        !c.isSingle && c.isAvailable === true && 
        c.age <= (personArray[i].age + num) && 
        c.age >= (personArray[i].age - num) ? k : p; 
       }, 0); 

       result = !personArray[result].isSingle && 
        personArray[result].sex !== personArray[i].sex && 
        personArray[result].age <= (personArray[i].age + num) && 
        personArray[result].age >= (personArray[i].age - num) ? result : -1; 

       if (result >= 0) { 
        householdArray.push (new Household (personArray[i], personArray[result])); 
        personArray[result].isAvailable = false; 
        personArray[i].isAvailable = false; 
       } 
      } 
     } 
     document.write("<br>Mancounter is: " + manCounter + 
       " loopCounter is: " + loopCounter + " households: " + householdArray.length); 
    } 
    while (manCounter > 0 && loopCounter <= 5); 
}; 

startMatchMaking(); 

CONTEXT:私はエージェントベースモデルの人口統計シミュレーションを実行するためのスタンドアロンアプリケーションを開発しようとしています。 personArrayは本質的に3万人の人間を含む。上記のコードの特定のビットは、母集団の初期設定に関連しています。 Personが事前に作成され、アレイにプッシュされました。各Personオブジェクトは、firstName,lastName,sex,ageおよびというプロパティを持ちます。彼らにはそれぞれランダムな値が割り当てられました。プログラムのこの段階では、私は単一ではないことを意味するPersonを持っていなければなりません。また、異性と適切な年齢の適切な仲間に合わせて世帯を形成する必要があります。

パフォーマンスを大幅に向上させるには、どうすれば最適化できますか?私は小さな変更や完全に異なる選択肢を開いて、同じものを出力しますresult

+1

はあなたのアレイコンパクトですか?すなわち、あなたはそれから要素を削除しますか?ローカル変数で 'personArray [i]'を捕捉して(おそらく) '3 * personArray.length'配列へのアクセスを避けることもできます。 – BeyelerStudios

+1

そのコードには「i」とは何ですか?その '.reduce()'呼び出しのコンテキストは何ですか?実際には、(現代のPC上で実行されているブラウザでは)30,000要素の配列を反復するには4秒かかります。 – Pointy

+3

別のループ内でそのコードを呼び出す場合は、*あなたの問題です。 – Pointy

答えて

1

私はこれをスピードアップするためにいくつかの前処理が必要だと思います。男性のソートされた配列上の年齢

  • 反復処理することにより

    • スプリット人口の男性と女性
    • ソート男性にのみ新しいマッチング女性の新しいセットを計算する:例えば

      年齢が処理されています

    • 最新のもので空ではなく最新のセットから女性を選択するだけです

    EDIT:小さな年齢差のカップルが最初に作成されるように、私たちは、必要に応じて、年齢差による女性のセットをソートすることにより、一致の数を最適化することができます。

    以下にいくつかのコード例を示します。他の人のよう

    var personArray = []; 
     
    
     
    // create test population 
     
    for(var n = 0; n < 30000; n++) { 
     
        personArray.push({ 
     
        isSingle: Math.random() < 0.5, 
     
        age: Math.round(18 + Math.random() * 80), 
     
        sex: Math.random() < 0.5 ? 'M' : 'F', 
     
        isAvailable: true 
     
        }); 
     
    } 
     
    
     
    var num = 7, // instead of num = normalRandomScaled(2.1, 12.44) 
     
        sex = [ [], [] ], 
     
        curAge = -1, subset, 
     
        houseHold = [], 
     
        ts = performance.now(); 
     
    
     
    // split population into men & women 
     
    personArray.forEach(function(p) { 
     
        sex[p.sex == 'M' ? 0 : 1].push(p); 
     
    }); 
     
    
     
    // sort men by age 
     
    sex[0].sort(function(a, b) { return a.age - b.age; }); 
     
    
     
    // iterate on men 
     
    sex[0].forEach(function(m) { 
     
        if(m.age != curAge) { 
     
        // create set of matching women for this age 
     
        subset = sex[1].filter(function(w) { 
     
         return w.isAvailable && w.isSingle && Math.abs(m.age - w.age) <= num; 
     
        }); 
     
        // sort by age difference, so that women with 
     
        // a small age difference are picked first 
     
        subset.sort(function(a, b) { 
     
         return Math.abs(m.age - b.age) - Math.abs(m.age - a.age); 
     
        }); 
     
        curAge = m.age; 
     
        } 
     
        if(m.isSingle && subset.length) { 
     
        // pick woman from set 
     
        var w = subset.pop(); 
     
        m.isAvailable = false; // (not really necessary) 
     
        w.isAvailable = false; 
     
        houseHold.push([ m, w ]); 
     
        } 
     
    }); 
     
    
     
    console.log(
     
        'Found ' + houseHold.length + ' matches ' + 
     
        'in ' + Math.round(performance.now() - ts) + 'ms' 
     
    ); 
     
    console.log(
     
        'Random example:', 
     
        houseHold[(Math.random() * houseHold.length) | 0] 
     
    );

  • +0

    興味深い、私はそれのようなもので動作すると思う。唯一のことは、私の 'householdArray'が異なるプロパティを持つ' Household'オブジェクトを含むことを本当に欲しかったということです。これらのプロパティのうちの2つは 'man'と' woman'で、これらの値は私の 'personArray'の適切な' Person'オブジェクトになります。 'Person'オブジェクトも' household'プロパティを持ち、その値は 'householdArray'からの適切な' Household'オブジェクトになります。こうして、私は 'personArray [x] .household.woman'や' household [x] .man.age'のようなことをすることができました。それは私の頭の中で非常にきちんと整理されています。 – neoflash

    +1

    @neoflash - 確かに。私はあなたの 'Household'オブジェクトの定義なしにスニペットとして実行できるように、元のコードを少し簡略化しました。 'householdArray.push(new Household(m、w))'を使うだけで、すべての設定が完了するはずです。 – Arnauld

    2

    reduceを使用して、このようにすべての要素を繰り返しループ内で繰り返します。ループ内では、既にコメントに記載されている要素を反復処理します。これは二次的な複雑さをもたらす。つまり、人数を2倍にすると、アルゴリズムの実行時間は4倍になります。このように、何百万人もの人を処理することは完全に不可能です。

    すべての要素の内側の反復は必要ないと思われます。 reduceを通常のループに置き換えて、一致が見つかったときに反復処理を停止することができます。現在の解決策は最後に見つかったマッチを取ります。最後のものを最初のものよりも良くするものはありますか?それとも私は何かが欠けていますか?マッチを検索するときにランダムにいくつかのインデックスを選び、マッチが見つかったときに停止するのはどうですか?これは多くの変更を必要としないソリューションであり、非常に若い人や非常に古い人(異常値)を除いて大きな違いを生み出すことを期待しています。より多くの変更を必要と

    ソリューションは、同様に、すでにコメントで述べたように、あなたがmatchCandidates = people[oppositeSex][ageWithSomeRandomness]ような何かを行うことができるようにそこ性質によって人々をマッピングすることです。 Javascriptのマップとハッシュテーブルの実装の可能性については、this postをご覧ください。何枚のシングルが含まれていないように

    追加の改善は、私は、最初に人をフィルタリングすることにより達成される可能性があります。 e。単一ではない人を新しい配列にコピーし、アルゴリズム内の新しい配列にのみアクセスします。

    コードがブラウザで実行されている場合は、web workersを使用して、ブラウザがフリーズしないようにすることができます。

    1

    と私はコメントで述べている:あなたのアプローチは、この場合には、入力サイズは二次でブルートフォース、です。最適化の可能性はいくつかあります。配列をカテゴリに分割するバイナリ値(すなわちブール値)については、些細なことである。年齢などの数値は、クラスタ化されている可能性があります。範囲に入れる。そして、あなたは、mm759で述べたように、早期打ち切りを決定的に採用すべきです。 TLDR:一番下に表と結論があります。

    は、(参考)ブルートフォースのアプローチを考えてみましょう:

    // The result is a list of matches [[candidate.id, match.id (or -1)]] 
    function bruteforce(arr) { 
        var matches = []; 
        for(var i = 0; i < arr.length; ++i) { 
        var candidate = arr[i], num = 12; 
        var isCandidate = !candidate.isSingle && candidate.isAvailable && candidate.sex == 1; 
        var cSex = candidate.sex; 
        var cSexPref = candidate.sexPref; 
        var cAgeMin = candidate.age - num; 
        var cAgeMax = candidate.age + num; 
        var result = !isCandidate ? -1 : arr.reduce(function(p,c,k,a){ 
         return k != i && 
         c.sex == cSexPref && 
         c.sexPref == cSex && 
         !c.isSingle && c.isAvailable && 
         c.age <= cAgeMax && 
         c.age >= cAgeMin ? k : p; 
        }, -1); 
        if(isCandidate) 
         matches.push([i, result]); 
        } 
        return matches; 
    } 
    

    カテゴリーアプローチは、次のようになります。

    function useTheCategory(arr) { 
        // preprocessing the data 
        var wmNonsingleAvailables = []; 
        var wwNonsingleAvailables = []; 
        var mwNonsingleAvailables = []; 
        var mmNonsingleAvailables = []; 
        // split the data into categories 
        arr.forEach(function(c) { 
        if(!c.isSingle && c.isAvailable) { 
         if(c.sex == 0) { 
         if(c.sexPref == 1) 
          wmNonsingleAvailables.push(c); 
         else 
          wwNonsingleAvailables.push(c); 
         } else { 
         if(c.sexPref == 0) 
          mwNonsingleAvailables.push(c); 
         else 
          mmNonsingleAvailables.push(c); 
         } 
        } 
        }); 
    
        var matches = []; 
        for(var i = 0; i < arr.length; ++i) { 
        var candidate = arr[i], num = 12; 
        var isCandidate = !candidate.isSingle && candidate.isAvailable && candidate.sex == 1; 
        var cSex = candidate.sex; 
        var cSexPref = candidate.sexPref; 
        var cAgeMin = candidate.age - num; 
        var cAgeMax = candidate.age + num; 
        if(isCandidate) { 
         var category = null; 
         // find the relevant category (in this case) 
         // a more complex approach/split might include multiple categories here 
         if(cSex == 0) { 
         if(cSexPref == 1) 
          category = mwNonsingleAvailables; 
         else if(cSexPref == 0) 
          category = wwNonsingleAvailables; 
         } else if(cSex == 1) { 
         if(cSexPref == 0) 
          category = wmNonsingleAvailables; 
         else if(cSexPref == 1) 
          category = mmNonsingleAvailables; 
         } 
         var result = -1; 
         if(category == null) { 
         // always handle the error case... 
         console.log("logic error: missing category!"); 
         console.log("candidate: " + JSON.stringify(candidate)); 
         } else { 
         // the tests for matching sex/single/availability are left-overs and not necessarily required, 
         // they are left in here to show that the reduce is not the culprit of your slowdown 
         var match = category.reduce(function(p,c,k,a){ 
          return c.id != i && 
          c.sex == cSexPref && 
          c.sexPref == cSex && 
          !c.isSingle && c.isAvailable && 
          c.age <= cAgeMax && 
          c.age >= cAgeMin ? k : p; 
         }, -1); 
         // translate to arr index 
         if(match != -1) 
          result = category[match].id; 
         } 
         matches.push([i, result]); 
        } 
        } 
        return matches; 
    } 
    

    そして年齢範囲バケットのアプローチは次のようになります。

    function useAgeRange(arr) { 
        // preprocessing the data 
        var ranges = [1, 2, 3, 4, 5]; // find appropriate ranges to spread the entries evenly (analyse your data, more below...) 
        var ageBuckets = []; 
        // find the range of age values 
        var ageRange = arr.length == 0 ? [0, 0] : arr.reduce(function(p,c) { 
        var min = c.age < p[0] ? c.age : p[0]; 
        var max = c.age > p[1] ? c.age : p[1]; 
        return [min, max]; 
        }, [arr[0].age, arr[0].age]); 
        // build the buckets (floor for nicer values) 
        for(var age = Math.floor(ageRange[0]), maxAge = ageRange[1], step = 0; age <= maxAge; age += step) { 
        // update step size 
        if(step == 0) 
         step = ranges[0]; 
        else 
         step = ranges[Math.min(ranges.length - 1, ranges.indexOf(step) + 1)]; 
        ageBuckets.push({ 
         nextAge: age + step, 
         bucket: [], 
        }); 
        } 
        function findBucketIndex(age) { 
        // min i with age < ageBuckets[i].nextAge 
        for(var i = 0, maxi = ageBuckets.length - 1; i < maxi; ++i) 
         if(age < ageBuckets[i + 1].nextAge) 
         return i; 
        return -1; 
        } 
        arr.forEach(function(c) { 
        ageBuckets[findBucketIndex(c.age)].bucket.push(c); 
        }); 
    
        var matches = []; 
        for(var i = 0; i < arr.length; ++i) { 
        var candidate = arr[i], num = 12; 
        var isCandidate = !candidate.isSingle && candidate.isAvailable && candidate.sex == 1; 
        var cSex = candidate.sex; 
        var cSexPref = candidate.sexPref; 
        var cAgeMin = candidate.age - num; 
        var cAgeMax = candidate.age + num; 
        if(isCandidate) { 
         // Find range intersection with ageBuckets 
         var startBucket = findBucketIndex(cAgeMin); 
         var endBucket = findBucketIndex(cAgeMax); 
         if(startBucket < 0) startBucket = 0; 
         if(endBucket < 0) endBucket = ageBuckets.length - 1; 
         var result = -1; 
         // now only search those candidate buckets 
         for(var b = startBucket; b <= endBucket; ++b) { 
         var bucket = ageBuckets[b].bucket; 
         var match = bucket.reduce(function(p,c,k,a){ 
          return c.id != i && 
          c.sex == cSexPref && 
          c.sexPref == cSex && 
          !c.isSingle && c.isAvailable && 
          c.age <= cAgeMax && 
          c.age >= cAgeMin ? k : p; 
         }, -1); 
         // translate to arr index 
         if(match >= 0) 
          result = bucket[match].id; 
         } 
         matches.push([i, result]); 
        } 
        } 
        return matches; 
    } 
    

    2つのアプローチの改善点を示すベンチマークを作成しましたon jsfiddle。どちらも、(でも前処理を含め、値はシステムやブラウザ間で異なります)単独では効果的である:適切であるアプローチを見つけるためにあなたのデータを分析

    N  Search space Brute force Categories Range buckets 
         (#matches)   (relative timing values) 
    20000 2500   200   34   140 
    40000 5000   1400   180   556 
    80000 10000   5335   659   2582 
    160000 20000   17000  2450  16900 
    

    がすべてです:私のベンチマークは指数分布を生成する(年齢18-20データポイントの28%、21-32別の27%、33-52別の27%、53-77の約18%を残している)。範囲のアプローチは、ほとんどのクエリで24の年齢範囲がデータの55%に及ぶため、上のタイミングで見るようにこの分布をうまく扱っていません(固定のnum = 12年と14のバケットの場合)。

    関連する問題