【问题标题】:JavaScript - Filter thousands of keys thousands of timesJavaScript - 数千次过滤数千个键
【发布时间】:2018-06-24 19:28:55
【问题描述】:

我的网络应用上有图表,每次收到新信息时我都需要更新图表。

现在我正在做模拟,所以我用大约 100000 个数据回测(在 json 中)(但如果浏览器和硬件可以处理它,可能是数百万个)。

因此,我需要尽可能优化我的代码。

我有这样的对象:

var trades = {"1515867288390":{price:"10", quantity:"500"},
            "1515867289541":{price:"9", quantity:"400"},
            "1515867295400":{price:"11", quantity:"750"},
            "1515867296500":{price:"7", quantity:"1100"},
            ...}

每次我在交易中扫描一个对象时,我都想获得最后 X 秒的中间价格,所以我有一个 $.each(trades, getAverage...)

getAverage = function (trade_time) {

var total_quantity = 0;
var total_trade_value = 0;
var startfrom = trade_time - duration;

Object.keys(trades).forEach(function (time) {
    if (time < startfrom)
        delete trades[time];
});

$.each(trades, function (key, value) {
    total_quantity += parseFloat(value.quantity);
    total_trade_value += (value.price * value.quantity);
});

var average = (total_trade_value / total_quantity);
return average;
}

80000 笔交易的平均执行时间约为 7.5 秒。

我猜不错,但问题是我需要var startfrom = trade_time - duration 中的持续时间可以调整,这会导致问题,因为我的 getAverage 函数会根据 startfrom 删除所有元素,它本身取决于持续时间,所以如果在开始持续时间 = 10,然后持续时间更改为 20,无论如何获取平均值只能回顾最后 10 秒。

一种解决方案是复制数组以保留“完整”副本,但我的函数每次都会迭代所有元素,而且速度会慢很多。 我尝试过的第二个选项是不删除该项目并使用:

Object.keys(trades).filter(t => t>=startfrom).forEach(function (time) {
    var value = trades[time];
    total_quantity += parseFloat(value.quantity);
    total_trade_value += (value.price * value.quantity);
});

它慢了大约 300 倍,真是糟糕的选择,我想知道你会怎么想?

谢谢。

PS:我正在考虑使用数组,因为我的键总是数字(时间戳),但是如果我使用数组,我最终会得到数百万个空索引,这不会再次降低性能吗?

【问题讨论】:

  • 你为什么使用$.each,这几乎肯定比使用等效的内置方法要慢?您已经完成了Object.keys(trades),因此请存储密钥并重新使用它们。 delete trades[type] 中的type 来自哪里?
  • 由于对象没有排序,你不能在任何有意义的意义上“返回”,你必须将键放入一个数组并对其进行排序,这样你就可以在一个一定的时间范围。最好将您的数据放入[{time:1515867288390, price:"10", quantity:"500"},...] 之类的数组中。如果按顺序推送,它们已经很容易排序,以便在一个范围内的交易中前后移动。
  • 在您当前的对象中,时间用于属性名称,因此具有相同时间值的第二次交易将覆盖第一次,即它不能有重复的时间值或两个相同的交易时间。最后一个获胜。
  • 对您的逻辑没有帮助,但性能的一个好步骤是将其移动到 webWorker 中,该 webWorker 将直接连接到套接字。这样你甚至可以在不污染主线程和用户体验的情况下处理内存垃圾。
  • Websocket 是有状态的,是吗?因此,在每笔交易到达时对其进行转换并将其推送到数组中(如果交易可能无序到达,则将其拼接到数组中)。不择手段,确保数组按时间戳顺序保存。然后,当您的非套接字代码从套接字(如trades)获取数据并计算出平均值时,您想要的数据将始终位于数组的一端。一旦达到不合格交易,计算就会停止(跳出循环)。

标签: javascript jquery performance ecmascript-6 javascript-objects


【解决方案1】:

如果将其转换为一个循环,而不是代码中的两个循环(一个用于删除,另一个用于迭代),像这样

if条件的逆

Object.keys(trades).forEach(function (time) {
    if (time >= startfrom) {
      value = trades[type];
      total_quantity += parseFloat(value.quantity);
      total_trade_value += (value.price * value.quantity);
    }

});

【讨论】:

  • 对于特定交易,OP 希望在入场后 x 秒内获得交易并平均价格(其中 x 是可调整的)。这只是遍历所有交易。
  • 这可能会快一点,但由于@RobG 所说的原因不适合我的问题
  • @user3119384 而不是两个循环,一个用于删除,一个用于迭代,您可以使用上述单个功能,因为它不会删除数组项,您可以进一步检查 20 秒或更多
  • 您提供的解决方案类似于我尝试的第二个测试,这个想法很好,适用于小对象,但如果您不删除元素,javascript 将迭代数组中的每个元素每次调用 getAverage 时,根据我的尝试,对于 1000 笔交易数据集,它最终会慢约 300 倍。
【解决方案2】:

也许低级实现更快。为此,您可以创建一个新的Buffer 来存储您的数据:

 const buffer = new ArrayBuffer(10 ** 4 * (3 * 3));

要实际使用缓冲区,我们需要对其进行查看。我认为 int32 足以存储时间戳、数量和数据(以 3 * 3 字节为单位)。所有可以捆绑在一个类中的东西:

 class TradeView {
  constructor(buffer, start, length){
   this.buffer = buffer;
   this.trades = new Uint32Array(buffer, start, length);
  }
  //...
}

现在要添加一笔交易,我们转到相关仓位,并将数据存储在那里:

   //TradeView.addTrade
   addTrade(index, timestamp, {quantity, price}){
    this.trades[index * 3] = +timestamp;
    this.trades[index * 3 + 1] = +price;
    this.trades[index * 3 + 2] = +quantity;
  }

或者得到它:

 //TradeView.getTrade
 getTrade(index){
   return {
     timestamp: this.trades[index * 3],
     price: this.trades[index * 3 + 1],
     quantity: this.trades[index * 3 + 2],
  };
}

现在我们需要用对象数据填充它(这很慢,所以当你从后端收到一个小块时应该调用它):

 const trades = new TradeView(buffer);
 let end = 0;

 function loadChunk(newTrades){
   for(const [timestamp, data] of Object.entries(newTrades))
     trades.addTrade(end++, timestamp, data);
}

现在真正酷的部分是:一个缓冲区可以有多个数据视图。这意味着,我们可以在不复制数据的情况下“过滤”交易数组。为此,我们只需要找到起始索引和结束索引:

 //TradeView.getRangeView
 getRangeView(startTime, endTime){
   let start = 0, end = 0;
   for(let i = 0; i < this.trades.length; i += 3){
      if(!start && startTime < this.trades[i])
         start = i;
      if(this.trades[i] > endTime){
         end = i - 3;
         break;
      }
  }
  return new TradeView(this.buffer, start, end - start);
}

【讨论】:

  • 如果我们在浏览器环境中,那么我猜你的意思是 ArrayBuffer,而10 ** 10 的长度无效,请注意它是TypedArray(buffer, start, length),而不是..., end) .否则,好主意。
  • 这个想法是天才,如果我理解它,因为我认为它应该很好用,但我认为 Roamer-1888 作为评论给出的解决方案更简单,并且适合我的问题。
  • @user3119384 策略也可以应用于这个答案
  • @JonasW。听起来更好,但您确定您的 getRangeView 功能吗?正如我所说,构造函数是TypedArray(buffer, start, length),而如果我阅读正确(没有运行,所以我可能是错的并且毫无理由地保持我的赞成票)你将length设置为结束偏移量,这在大多数情况下都是错误的。
  • @kaiido 再次感谢。你是绝对正确的,也没有测试过......
【解决方案3】:

这里有几个(密切相关的)想法。

想法 1

当每笔交易到达时,将其推送到数组中(如果交易可能乱序到达,则将其拼接到数组中)。不择手段,确保数组按时间戳顺序保存。然后,当您的非套接字代码从套接字获取数据(作为交易)并计算出平均值时,您想要的数据将始终位于数组的一端。一旦达到不合格交易,计算就会停止(跳出循环)。

想法 2

与想法 1 类似,但不是维护一系列原始交易,而是存储一系列“统计对象”,每个对象代表一个时间片 - 可能只有 15 秒的交易,但可能长达 5 分钟。

在每个 stats 对象中,聚合 trade.quantitytrade.quantity * trade.price。这将允许计算时间片的平均值,但更重要的是,在计算平均值之前,可以通过简单的相加来组合两个或多个时间片。

这可以通过两个相互依赖的构造函数来实现:

/*
 * Stats_store() Constructor
 * Description: 
 *    A constructor, instances of which maintain an array of Stats_store() instances (each representing a time-slice), 
 *    and receive a series of timestamped "trade" objects of the form { price:"10", quantity:"500" }.
 *    On receipt of a trade object, an exiting Stats_store() instance is found (by key based on timestamp) or a new one is created,
 *    then the found/created Stats_store's .addTrade(trade)` method is called.
 * Methods: 
 *    .addTrade(timestamp, trade): called externally
 *    .getMean(millisecondsAgo): called externally
 *    .timeStampToKey(timestamp): called internally
 *    .findByKey(key): called internally
 * Example: var myStats_store = new Stats_store(101075933);
 * Usage: 
 */
const Stats_store = function(granularity) {
    this.buffer = [];
    this.granularity = granularity || 60000; // milliseconds (default 1 minute)
};
Stats_store.prototype = {
    'addTrade': function(timestamp, trade) {
        let key = this.timeStampToKey(timestamp);
        let statObj = this.findByKey(key);
        if (!statObj) {
            statObj = new StatObj(key);
            this.buffer.unshift(statObj);
        }
        statObj.addTrade(trade);
        return this;
    },
    'timeStampToKey': function (timestamp) {
        // Note: a key is a "granulated" timestamp - the leading edge of a timeslice.
        return Math.floor(timestamp / this.granularity); // faster than parseInt()
    },
    'findByKey': function(key) {
        for(let i=0; i<this.buffer.length; i++) {
            if(this.buffer[i].key === key) {
                return this.buffer[i];
                break;
            }
            return null;
        }
    },
    'getMean': function(millisecondsAgo) {
        let key = this.timeStampToKey(Date.now() - millisecondsAgo);
        let s = { 'n':0, 'sigma':0 };
        let c = 0;
        for(let i=0; i<this.buffer.length; i++) {
            if(this.buffer[i].isFresherThan(key)) {
                s.n += this.buffer[i].n;
                s.sigma += this.buffer[i].sigma;
                c++;
            } else {
                break;
            }
        }
        console.log(c, 'of', this.buffer.length);
        return s.sigma / s.n; // arithmetic mean
    }
};

/*
 * StatObj() Constructor
 * Description: 
 *    A stats constructor, instances of which receive a series of "trade" objects of the form { price:"10", quantity:"500" }.
 *    and to aggregate data from the received trades:
 *       'this.key': represents a time window (passes on construction).
 *       'this.n': is an aggregate of Σ(trade.quantity)
 *       'this.sigma' is an aggregate of trade values Σ(trade.price * trade.quantity)
 *    Together, 'n' and 'sigma' are the raw data required for (or contributing to) an arithmetic mean (average).
 *    NOTE: If variance or SD was required, then the store object would need to accumulate 'sigmaSquared' in addition to 'n' and 'sigma'.
 * Methods: 
 *    .addTrade(trade): called externally
 *    .isFresherThan(key): called externally
 * Example: var myStaObj = new StatObj(101075933);
 * Usage: should only be called by Stats_store()
 */
const StatObj = function(key) {
    this.key = key;
    this.n = 0;
    this.sigma = 0;
}
StatObj.prototype = {
    'addTrade': function(trade) { // eg. { price:"10", quantity:"500" }
        this.n += +trade.quantity;
        this.sigma += +trade.quantity * +trade.price;
    },
    'isFresherThan': function(key) {
        return this.key >= key;
    }
};

用法

// Initialisation
let mySocket = new WebSocket("ws://www.example.com/socketserver", "protocolOne");
const stats_store = new Stats_store(2 * 60000); // 2 minutes granularity

// On receiving a new trade (example)
mySocket.onmessage = function(event) {
    let trade = ....; // extract `trade` from event
    let timestamp = ....; // extract `timestamp` from event
    let mean = stats_store.addTrade(timestamp, trade).getMean(10 * 60000); // 10 minutes averaging timeslice.
    console.log(mean); // ... whatever you need to do with the calculated mean.
    // ... whatever else you need to do with `trade` and `timestamp`.
};

通过选择传递给new Stats_store().getMean() 的值提供了一定程度的灵活性。只需确保第一个值小于第二个值即可。

(2)here

轻度测试(在中等性能电脑上,Win7下的Chrome浏览器上)表明:

  • 性能至少应该足以满足您所说的那种“交易”率(12 小时内 100,000 次或每分钟 140 次)。
  • 内存使用率很高,但短期内不会泄漏。从长远来看,您可能需要一个“管家”流程来扫尾。

最后,想法(1)和(2)并不完全不同。

由于 (2) 的 granularity 常量传递给 new Stats_store() 变得更小,因此 (2) 的行为将倾向于 (1) 的行为。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-09-26
    • 1970-01-01
    • 2021-10-22
    • 2019-07-30
    • 2017-09-09
    • 1970-01-01
    • 1970-01-01
    • 2012-10-03
    相关资源
    最近更新 更多