@Override
    public SelectedFeatureSet select(Dataset<Label> dataset) {
        FSMatrix data = FSMatrix.buildMatrix(dataset,numBins);
        ImmutableFeatureMap fmap = data.getFeatureMap();
        int max = k == -1 ? fmap.size() : Math.min(k,fmap.size());
        int numFeatures = fmap.size();

        boolean[] unselectedFeatures = new boolean[numFeatures];
        Arrays.fill(unselectedFeatures, true);
        int[] selectedFeatures = new int[max];
        double[] selectedScores = new double[max];

        double[] miCache;
        int[] idxCache = new int[numFeatures];

        if (numThreads > 1) {
            ForkJoinPool fjp = new ForkJoinPool(numThreads);
            try {
                miCache = fjp.submit(() -> IntStream.range(0, numFeatures).parallel().mapToDouble(data::mi).toArray()).get();
            } catch (InterruptedException | ExecutionException e) {
                throw new RuntimeException(e);
            }
            fjp.shutdown();
        } else {
            miCache = IntStream.range(0, numFeatures).mapToDouble(data::mi).toArray();
        }

        int curIdx = -1;
        double curVal = -1.0;
        for (int i = 0; i < numFeatures; i++) {
            if (miCache[i] > curVal) {
                curIdx = i;
                curVal = miCache[i];
            }
        }

        selectedFeatures[0] = curIdx;
        selectedScores[0] = curVal;
        unselectedFeatures[curIdx] = false;
        logger.log(Level.INFO,"Itr 0: selected feature " + fmap.get(curIdx).getName() + ", score = " + selectedScores[0]);

        //
        // Select features in max CMIM order using fast algorithm
        for (int i = 1; i < max; i++) {
            double curMaxVal = -1.0;
            int curMaxIdx = -1;
            for (int j = 0; j < numFeatures; j++) {
                if (unselectedFeatures[j]) {
                    while ((miCache[j] > curMaxVal) && (idxCache[j] < i)) {
                        double newVal = data.cmi(j, selectedFeatures[idxCache[j]]);
                        if (newVal < miCache[j]) {
                            miCache[j] = newVal;
                        }
                        idxCache[j]++;
                    }
                    if (miCache[j] > curMaxVal) {
                        curMaxVal = miCache[j];
                        curMaxIdx = j;
                    }
                }
            }
            selectedFeatures[i] = curMaxIdx;
            selectedScores[i] = curMaxVal;
            unselectedFeatures[curMaxIdx] = false;
            logger.log(Level.INFO,"Itr " + i + ": selected feature " + fmap.get(curMaxIdx).getName() + ", score = " + curMaxVal);
        }

        ArrayList<String> names = new ArrayList<>();
        ArrayList<Double> scores = new ArrayList<>();
        for (int i = 0; i < max; i++) {
            names.add(fmap.get(selectedFeatures[i]).getName());
            scores.add(selectedScores[i]);
        }

        FeatureSetProvenance provenance = new FeatureSetProvenance(SelectedFeatureSet.class.getName(),dataset.getProvenance(),getProvenance());
        return new SelectedFeatureSet(names,scores,isOrdered(),provenance);
    }
@Test
    public void parallelismTest() {
        Dataset<Label> dataset = MIMTest.createDataset();

        CMIM sequential = new CMIM(-1,5,1);
        SelectedFeatureSet sequentialSet = sequential.select(dataset);

        CMIM parallel = new CMIM(-1,5,4);
        SelectedFeatureSet parallelSet = parallel.select(dataset);

        assertEquals(sequentialSet.featureNames(),parallelSet.featureNames());
        double[] sequentialScores = Util.toPrimitiveDouble(sequentialSet.featureScores());
        double[] parallelScores = Util.toPrimitiveDouble(parallelSet.featureScores());
        assertArrayEquals(sequentialScores,parallelScores,1e-15);
    }