Giter Site home page Giter Site logo

caiogondim / fast-memoize.js Goto Github PK

View Code? Open in Web Editor NEW
2.6K 36.0 86.0 1.32 MB

:rabbit2: Fastest possible memoization library

Home Page: https://npm.im/fast-memoize

License: MIT License

JavaScript 95.98% Shell 0.99% TypeScript 3.03%
javascript memoization js

fast-memoize.js's Introduction

fast-memoize

 Travis CI Code coverage

In computing, memoization is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. — Wikipedia

This library is an attempt to make the fastest possible memoization library in JavaScript that supports N arguments.

Installation

npm install fast-memoize --save

Usage

const memoize = require('fast-memoize')

const fn = function (one, two, three) { /* ... */ }

const memoized = memoize(fn)

memoized('foo', 3, 'bar')
memoized('foo', 3, 'bar') // Cache hit

Custom cache

The fastest cache is used for the running environment, but it is possible to pass a custom cache to be used.

const memoized = memoize(fn, {
  cache: {
    create() {
      var store = {};
      return {
        has(key) { return (key in store); },
        get(key) { return store[key]; },
        set(key, value) { store[key] = value; }
      };
    }
  }
})

The custom cache should be an object containing a create method that returns an object implementing the following methods:

  • get
  • set
  • has

Custom serializer

To use a custom serializer:

const memoized = memoize(fn, {
  serializer: customSerializer
})

The serializer is a function that receives one argument and outputs a string that represents it. It has to be a deterministic algorithm meaning that, given one input, it always returns the same output.

Benchmark

For an in depth explanation on how this library was created, go read this post on RisingStack.

Below you can see a performance benchmark between some of the most popular libraries for memoization.

To run the benchmark, clone the repo, install the dependencies and run npm run benchmark.

git clone [email protected]:caiogondim/fast-memoize.git
cd fast-memoize
npm install
npm run benchmark

Against another git hash

To benchmark the current code against a git hash, branch, ...

npm run benchmark:compare 53fa9a62214e816cf8b5b4fa291c38f1d63677b9

Gotchas

Rest & Default Parameters

We check for function.length to get upfront the expected number of arguments in order to use the fastest strategy. But when rest & default parameters are being used, we don't receive the right number of arguments (see details).

// Rest parameter example
function multiply (multiplier, ...theArgs) {
  return theArgs.map(function (element) {
    return multiplier * element
  })
}
multiply.length // => 1

// Default parameter example
function divide (element, divisor = 1) {
  return divisor * element
}
divide.length // => 1

So if you use rest & default parameters, explicitly set the strategy to variadic.

const memoizedMultiply = memoize(multiply, {
  strategy: memoize.strategies.variadic
})

Function Arguments

The default serializer uses JSON.stringify which will serialize functions as null. This means that if you are passing any functions as arguments you will get the same output regardless of whether you pass in different functions or indeed no function at all. The cache key generated will always be the same. To get around this you can give each function a unique ID and use that.

let id = 0
function memoizedId(x) {
  if (!x.__memoizedId) x.__memoizedId = ++id
  return { __memoizedId: x.__memoizedId }
}

memoize((aFunction, foo) => {
  return aFunction.bind(foo)
}, {
  serializer: args => {
    const argumentsWithFuncIds = Array.from(args).map(x => {
      if (typeof x === 'function') return memoizedId(x)
      return x
    })
    return JSON.stringify(argumentsWithFuncIds)
  }
})

Credits


caiogondim.com  ·  GitHub @caiogondim  ·  Twitter @caio_gondim

fast-memoize.js's People

Contributors

aduth avatar aladdin-add avatar allenfang avatar amilajack avatar anywhichway avatar benfogel avatar caiogondim avatar cap32 avatar comerc avatar disintegrator avatar elektronik2k5 avatar elliotthill avatar getify avatar larkinscott avatar leobalter avatar msaspence avatar piperchester avatar rafael-lima avatar ruyadorno avatar slavaganzin avatar tdev9 avatar tohjustin avatar yenbekbay avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fast-memoize.js's Issues

Object literal property shorthand breaks UglifyJS with Webpack.

Hey there! I just noticed when attempting to run this library through Webpack's UglifyJS plugin that index.js:19 causes it to choke. Apparently the object literal property shorthand isn't supported yet, causing UglifyJS to throw this error: SyntaxError: Unexpected token punc «,», expected punc «:».

Would it be possible to use the older, more verbose syntax in this case? Thanks in advance for considering it.

Does not appear to be the fastest.

We were just getting ready to release our memoization code when yours was announced on Node Weekly. Ours also handles N arguments and appears to be faster than fast-memoize or lodash, although we have a lot more testing we want to do, e.g. comparisons of scalability with 1,2,3,4,5 arguments. We also don't handle objects as arguments properly at this time. Here at AnyWhichWay we believe choice and variety are really good for the community, but you might want to tame down your claim about being the fastest. Here is the link: https://github.com/anywhichway/iMemoized.

Would be great to have a TTL

That way cache entries will automatically expire and not explode memory usage. This is especially important on the server.

This would likely involve you keeping a separate "shadow cache" that kept the created times of the cacheKeys.

Obviously this would be fractionally slower so a simple opt-in (by say {TTL: [any non-zero value in ms]} in the options) to this code path is probably all you need.

If you're interested I can submit a PR ;)

Doesn't work with inputs that have default value declaration.

for example:

const foo = (bool, num = 5) => bool ? {num} : {def: "default"}
expect(foo.length).toEqual(2) //Error: foo.length === 1 instead of 2

const memFoo = memoize(foo);
expect(memFoo(true, 5) === memFoo(true, 6)).toBe(false) //Error is true
expect(memFoo(true) === memFoo(true, 6)).toBe(false) //Error is true

Maybe add this to the limitation section in docs with the workaround

const foo = (bool, num) => {
   num = num || 5; //initialize default value here
   return bool ? {num} : {def: "default"}
}

Add Promise support

Working on a version of this on my fork now but it would be great to be able to pass in a promise and have the library return a promise that resolved to that result.

ie:

var foo = memoize(myPromiseReturningFunc);

foo(a, b, c).then((data) => {
    // do something with the data...
});

Per JS spec the official way of doing this check would be typeof fn.then == "function" as that makes the function a "thenable".

Thoughts, concerns, etc? I may open a PR if I get it in fast enough ;)

Suggestion: use array instead of arguments.

So the default serializer does this:

function jsonStringify () {
  return JSON.stringify(arguments)
}

For cache checks (ie foo._cache.has(bar)) this makes it almost useless. It would be much easier if it converted the passed arguments object to an array and then stringified that.

function jsonStringify (args) {
  return JSON.stringify(Array.prototype.slice.call(args))
}

This would allow a key generation of just let bar = JSON.stringify(myArgsArray) which is currently only achievable by initially function wrapping to get the arguments pseudo-array.

I'm rolling with this as a custom serializer atm with no issue, would be very nice to not have to though as it presents no downside and gets away from use of the horrible arguments object too.

configure cache size

Is it possible to configure the max cache size?
I took a look at the source and noticed that the simple cache (Object cache) applies no limits.
I am not very comfortable to use knowing that memory could eventually blow up the browser in a matter of time.

Unrealistic / flawed cache benchmarking

The cache benchmarking has a few issues:

  1. It only benchmarks small integers as keys of the underlying cache. This is my primary issue with the benchmarking. In my experience, I use memoization to avoid processing large in memory data structures. If the memoization function stringifies the incoming argument everytime, the stringification cost is highly likely to outweigh the performance benefits of memoization. Map is ideal for caching based on an object argument because v8 and other engines assign a random ID to every object under the hood and checking if it is the same object is as simple as comparing two ints. The cache benchmarking code should test massive objects as arguments to the memoization function.
  2. It does not model key deletion. Map is specifically designed with key deletion in mind whereas hidden classes on objects are not so well suited to this. Keys are likely to be added and deleted a lot in a memoized function so it's important to model this.
  3. It does not model the potential non-local performance impacts of using objects. In v8 the "megamorphic" cache system can become overloaded by the number of hidden classes and deoptimize code. I'm no v8 expert but I know enough to beware of microbenchmarking.

Evaluate benchmark results

I'm running benchmarks on

MacBook-Pro-di-Loreto:fast-memoize.js loretoparisi$ node -v
v6.9.4
MacBook-Pro-di-Loreto:fast-memoize.js loretoparisi$ npm -v
3.10.10
MacBook-Pro-di-Loreto:fast-memoize.js loretoparisi$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.12.2
BuildVersion:	16C67

My benchmarks are

┌──────────────────────┬────────────┬──────────────────────────┬─────────────┐
│ NAME                 │ OPS/SEC    │ RELATIVE MARGIN OF ERROR │ SAMPLE SIZE │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ iMemoized            │ 17,932,713 │ ± 3.37%                  │ 76          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ lodash               │ 8,396,854  │ ± 2.55%                  │ 80          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ fast-memoize@current │ 8,214,633  │ ± 1.99%                  │ 75          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ memoizee             │ 7,386,302  │ ± 2.33%                  │ 83          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ underscore           │ 6,429,656  │ ± 1.42%                  │ 82          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ ramda                │ 332,289    │ ± 2.15%                  │ 79          │
├──────────────────────┼────────────┼──────────────────────────┼─────────────┤
│ vanilla              │ 99,607     │ ± 2.81%                  │ 81          │
└──────────────────────┴────────────┴──────────────────────────┴─────────────┘

How to further improve fast-memoize on this machine?

.has() method for custom cache is never called. Should be removed from documentation?

I tried to use this memoizer for fuction with 5 arguments, and I implemented the custom cache with .get() .set() and .has() methods

We have 100% test coverage rule for our code. And after running tests I see that .has() method is never called. I checked the source code for fast-memoize and I see no usage of .has() method at all.

So, either documentation should be updated to not mention the .has() method as it is not required, or the code was overoptimized and instead of calling .has() and then .get() you assume that if .get() returns undefined, it means that it wasn't stored. If that's the case, please add this to documentation to manage expectations of library users better.

Doesn't memoize the result for keys returning `undefined`

When the memoized function returns undefined for a specific parameter, another call to the memoized function runs the original function again as opposed to fetching the result from the cache.

Reproducible Example:

const memoize = require('fast-memoize')
const assert = require('assert')

let numCalled = 0
function longProcess(arg) {
  ++numCalled
  return
}

// memoize result
const memoizedProcess = memoize(longProcess)
memoizedProcess("foo")

// get memoized result
memoizedProcess("foo")
assert(numCalled === 1)

It seems like this changed from v2.2.8 to v2.3.0. Is this the intended behaviour?

serializing class instances for 2+ arguments

reading through the code, I found this:

if (arguments.length === 1) {
  cacheKey = arguments[0]
} else {
  cacheKey = serializer(arguments)
}

given that the default serializer uses JSON.stringify()

function jsonStringify () {
  return JSON.stringify(arguments)
}

you'll potentially get wrong cache-hits for any memoized function calls that accept class instances (that don't "expose" all their props) as their arguments (given that you provide more than one argument).


example

// prep

class LotteryTicket {
  constructor(code) {
    Object.defineProperty(this, 'code', {
      enumerable: false,
      configurable: false,
      writable: false,
      value: code,
    });
  }

  check(code) {
    return this.code === code;
  }
}

function checkWinner(ticket, code) {
  return ticket.check(code);
}

var memoizedCheckWinner = memoize(checkWinner);

// test

memoizedCheckWinner(new LotteryTicket('xxx'), 'xxx'); // true, correct
// serialized cacheKey: {"0":{"0":{},"1":"xxx"}}

memoizedCheckWinner(new LotteryTicket('yyy'), 'xxx'); // true; incorrect
// serialized cacheKey: {"0":{"0":{},"1":"xxx"}}; wrong cache hit

Default argument breaks correct memoization

I am running this in webpack and have tried on requirebin.

const fastMemoize = require('fast-memoize');
                            
const memoConcat = fastMemoize((arg1, arg2 = 'default') => {
  // fastMemoize does not work correctly with default arguments
  const value = arg1 + '-' + arg2;
  return value;
});

console.log(memoConcat('foo', 'bar')); // -> incorrectly returns 'foo-default'

cacheDefault is not defined

Anyone seen this? It happens as soon as I load my app.

Uncaught ReferenceError: cacheDefault is not defined
    at memoize (eval at <anonymous> (vendor.js:2571), <anonymous>:8:7)
    at eval (eval at <anonymous> (commons.js:72), <anonymous>:171:87)
    at Object.<anonymous> (commons.js:72)
    at __webpack_require__ (vendor.js:692)
    at fn (vendor.js:111)
    at eval (eval at <anonymous> (commons.js:84), <anonymous>:8:66)
    at Object.<anonymous> (commons.js:84)
    at __webpack_require__ (vendor.js:692)
    at fn (vendor.js:111)
    at eval (eval at <anonymous> (commons.js:1694), <anonymous>:8:69)

My call to it is pretty simple:

import memoize from 'fast-memoize';
export const haystackMatchesSearchObject = memoize(_haystackMatchesSearchObject);

My webpack config is quite complex though, with hot reloading, I wonder if that's the cause

Typings for `Cache.Create`option

In the typings, the cache is expecting an object that not includes a Create method. It's correct? What's the right way to use it with Typescript?

export interface Cache<K, V> {
  get(key: K): V;
  set(key: K, value: V): void;
  has(key: K): boolean;
}

export type Serializer = (args: any[]) => string;

export interface Options<F extends Func> {
  cache?: Cache<string, ReturnType<F>>;
  serializer?: Serializer;
  strategy?: MemoizeFunc;
}

The tslint error output:

Argument of type '{ cache: { create(): { has(key: any): boolean; get(key: any): any; set(key: any, value: any): void; }; }; }' is not assignable to parameter of type 'Options<() => string>'.
  Types of property 'cache' are incompatible.
    Type '{ create(): { has(key: any): boolean; get(key: any): any; set(key: any, value: any): void; }; }' is not assignable to type 'Cache<string, string>'.
      Object literal may only specify known properties, and 'create' does not exist in type 'Cache<string, string>'.ts(2345)

[bug] TypeScript typings not published to npm

Steps to reproduce:

  1. Install in TS project with strict conf.
  2. Import
  3. Error 😞
Could not find a declaration file for module 'fast-memoize'. '[path-to-TS-project]/node_modules/fast-memoize/src/index.js' implicitly has an 'any' type.
  Try `npm install @types/fast-memoize` if it exists or add a new declaration (.d.ts) file containing `declare module 'fast-memoize';`

Fixed in #69

Not working with es6 getters

I have the following code

	get scale() {
		const createScale = (root, scaleType, noteType) => {
			console.log('MEMOIZE', root, scaleType, noteType);
			return new Scale(root, scaleType, noteType);
		};

		return memoize(createScale)(this._root, this._scaleType, this._noteType);
	}

Even though the arguments are the same, the createScale function is still called twice, thus returning different instances of Scale. Any ideas? Is this specific to ES6 class getters?

JSON.stringify

I think Json.stringify is too slow when arguments has big list object,it will take a long time to parse from object to string,in this situation, new Map() is a 10x faster than Json.stringify,maybe can change the way which strategy to choose by arguments types

more tests?

seems like some edge cases are not covered, can we add some more tests?

Add support of ES6 Modules

I would like to request the new feature to add support of ES6 Modules, which are becoming the de-facto standard to package JavaScript code.

This would allow to use fast-memoize natively in modern browsers without any build steps.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.