Improve performance of deduplication of sorted arrays

This commit is contained in:
Ron Buckton 2017-10-26 17:51:09 -07:00
parent d9775cd822
commit 3cb15378d7
10 changed files with 98 additions and 66 deletions

View File

@ -7341,24 +7341,12 @@ namespace ts {
unionIndex?: number;
}
function getTypeId(type: Type) {
return type.id;
}
function binarySearchTypes(types: Type[], type: Type): number {
let low = 0;
let high = types.length - 1;
const typeId = type.id;
while (low <= high) {
const middle = low + ((high - low) >> 1);
const id = types[middle].id;
if (id === typeId) {
return middle;
}
else if (id > typeId) {
high = middle - 1;
}
else {
low = middle + 1;
}
}
return ~low;
return binarySearch(types, type, getTypeId, compareValues);
}
function containsType(types: Type[], type: Type): boolean {

View File

@ -660,35 +660,77 @@ namespace ts {
}
/**
* Creates a new array with duplicate entries removed.
* Deduplicates an array that has already been sorted.
*/
export function deduplicateSorted<T>(array: SortedReadonlyArray<T>, comparer: EqualityComparer<T> | Comparer<T>) {
if (!array) return undefined;
if (array.length === 0) return [];
let last = array[0];
const deduplicated: T[] = [last];
for (let i = 1; i < array.length; i++) {
switch (comparer(last, array[i])) {
// equality comparison
case true:
// relational comparison
case Comparison.LessThan:
case Comparison.EqualTo:
continue;
}
deduplicated.push(last = array[i]);
}
return deduplicated;
}
/**
* Deduplicates an unsorted array.
* @param equalityComparer An optional `EqualityComparer` used to determine if two values are duplicates.
* @param comparer An optional `Comparer` used to sort entries before comparison. If supplied,
* results are returned in the original order found in `array`.
*/
export function deduplicate<T>(array: ReadonlyArray<T>, equalityComparer?: EqualityComparer<T>, comparer?: Comparer<T>): T[] {
if (!array) return undefined;
if (!comparer) return addRangeIfUnique([], array, equalityComparer);
return deduplicateWorker(array, equalityComparer, comparer);
export function deduplicate<T>(array: ReadonlyArray<T>, equalityComparer: EqualityComparer<T>, comparer?: Comparer<T>): T[] {
return !array ? undefined :
array.length === 0 ? [] :
array.length === 1 ? array.slice() :
comparer ? deduplicateRelational(array, equalityComparer, comparer) :
deduplicateEquality(array, equalityComparer);
}
function deduplicateWorker<T>(array: ReadonlyArray<T>, equalityComparer: EqualityComparer<T> = equateValues, comparer: Comparer<T>) {
function deduplicateRelational<T>(array: ReadonlyArray<T>, equalityComparer: EqualityComparer<T>, comparer: Comparer<T>) {
// Perform a stable sort of the array. This ensures the first entry in a list of
// duplicates remains the first entry in the result.
const indices = sequence(0, array.length);
stableSortIndices(array, indices, comparer);
const deduplicated: number[] = [];
loop: for (const sourceIndex of indices) {
for (const targetIndex of deduplicated) {
if (equalityComparer(array[sourceIndex], array[targetIndex])) {
continue loop;
}
let last = array[indices[0]];
const deduplicated: number[] = [indices[0]];
for (let i = 1; i < indices.length; i++) {
const index = indices[i];
const item = array[index];
if (!equalityComparer(last, item)) {
deduplicated.push(index);
last = item;
}
deduplicated.push(sourceIndex);
}
// return deduplicated items in original order
return deduplicated.sort().map(i => array[i]);
// restore original order
deduplicated.sort();
return deduplicated.map(i => array[i]);
}
function deduplicateEquality<T>(array: ReadonlyArray<T>, equalityComparer: EqualityComparer<T>) {
const result: T[] = [];
for (const item of array) {
pushIfUnique(result, item, equalityComparer);
}
return result;
}
export function sortAndDeduplicate<T>(array: ReadonlyArray<T>, comparer: Comparer<T>, equalityComparer?: EqualityComparer<T>) {
return deduplicateSorted(sort(array, comparer), equalityComparer || comparer);
}
export function arrayIsEqualTo<T>(array1: ReadonlyArray<T>, array2: ReadonlyArray<T>, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean {
@ -827,15 +869,6 @@ namespace ts {
return to;
}
function addRangeIfUnique<T>(to: T[], from: ReadonlyArray<T>, equalityComparer?: EqualityComparer<T>): T[] | undefined {
for (let i = 0; i < from.length; i++) {
if (from[i] !== undefined) {
pushIfUnique(to, from[i], equalityComparer);
}
}
return to;
}
/**
* @return Whether the value was added.
*/
@ -878,13 +911,20 @@ namespace ts {
indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y));
}
/**
* Returns a new sorted array.
*/
export function sort<T>(array: ReadonlyArray<T>, comparer: Comparer<T>) {
return array.slice().sort(comparer) as ReadonlyArray<T> as SortedReadonlyArray<T>;
}
/**
* Stable sort of an array. Elements equal to each other maintain their relative position in the array.
*/
export function stableSort<T>(array: ReadonlyArray<T>, comparer: Comparer<T>) {
const indices = sequence(0, array.length);
stableSortIndices(array, indices, comparer);
return indices.map(i => array[i]);
return indices.map(i => array[i]) as ReadonlyArray<T> as SortedReadonlyArray<T>;
}
export function rangeEquals<T>(array1: ReadonlyArray<T>, array2: ReadonlyArray<T>, pos: number, end: number) {
@ -969,25 +1009,22 @@ namespace ts {
* @param array A sorted array whose first element must be no larger than number
* @param number The value to be searched for in the array.
*/
export function binarySearch<T>(array: ReadonlyArray<T>, value: T, comparer?: Comparer<T>, offset?: number): number {
export function binarySearch<T, U>(array: ReadonlyArray<T>, value: T, keySelector: Selector<T, U>, keyComparer: Comparer<U>, offset?: number): number {
if (!array || array.length === 0) {
return -1;
}
let low = offset || 0;
let high = array.length - 1;
comparer = comparer !== undefined
? comparer
: (v1, v2) => (v1 < v2 ? -1 : (v1 > v2 ? 1 : 0));
const key = keySelector(value);
while (low <= high) {
const middle = low + ((high - low) >> 1);
const midValue = array[middle];
const midKey = keySelector(array[middle]);
if (comparer(midValue, value) === 0) {
if (keyComparer(midKey, key) === 0) {
return middle;
}
else if (comparer(midValue, value) > 0) {
else if (keyComparer(midKey, key) > 0) {
high = middle - 1;
}
else {
@ -2452,8 +2489,8 @@ namespace ts {
return flatten<string>(results);
function visitDirectory(path: string, absolutePath: string, depth: number | undefined) {
let { files, directories } = getFileSystemEntries(path);
files = files.slice().sort(comparer);
const entries = getFileSystemEntries(path);
const files = sort(entries.files, comparer);
for (const current of files) {
const name = combinePaths(path, current);
@ -2478,7 +2515,7 @@ namespace ts {
}
}
directories = directories.slice().sort(comparer);
const directories = sort(entries.directories, comparer);
for (const current of directories) {
const name = combinePaths(path, current);
const absoluteName = combinePaths(absolutePath, current);

View File

@ -352,7 +352,7 @@ namespace ts {
* We assume the first line starts at position 0 and 'position' is non-negative.
*/
export function computeLineAndCharacterOfPosition(lineStarts: ReadonlyArray<number>, position: number): LineAndCharacter {
let lineNumber = binarySearch(lineStarts, position);
let lineNumber = binarySearch(lineStarts, position, identity, compareValues);
if (lineNumber < 0) {
// If the actual position was not found,
// the binary search returns the 2's-complement of the next line start

View File

@ -306,7 +306,7 @@ namespace ts {
// Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch")
const optsList = showAllOptions ?
optionDeclarations.slice().sort((a, b) => compareStringsCaseInsensitive(a.name, b.name)) :
sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) :
filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView);
// We want our descriptions to align at the same column in our output,

View File

@ -36,6 +36,11 @@ namespace ts {
push(...values: T[]): void;
}
/* @internal */
export interface SortedReadonlyArray<T> extends ReadonlyArray<T> {
" __sortedArrayBrand": any;
}
/* @internal */
export type EqualityComparer<T> = (a: T, b: T) => boolean;

View File

@ -321,16 +321,16 @@ namespace ts {
return getSourceTextOfNodeFromSourceFile(getSourceFileOfNode(node), node, includeTrivia);
}
function getPos(range: Node) {
return range.pos;
}
/**
* Note: it is expected that the `nodeArray` and the `node` are within the same file.
* For example, searching for a `SourceFile` in a `SourceFile[]` wouldn't work.
*/
export function indexOfNode(nodeArray: ReadonlyArray<Node>, node: Node) {
return binarySearch(nodeArray, node, compareNodePos);
}
function compareNodePos({ pos: aPos }: Node, { pos: bPos}: Node) {
return aPos < bPos ? Comparison.LessThan : bPos < aPos ? Comparison.GreaterThan : Comparison.EqualTo;
return binarySearch(nodeArray, node, getPos, compareValues);
}
/**

View File

@ -1313,7 +1313,7 @@ namespace Harness {
export const diagnosticSummaryMarker = "__diagnosticSummary";
export const globalErrorsMarker = "__globalErrors";
export function *iterateErrorBaseline(inputFiles: ReadonlyArray<TestFile>, diagnostics: ReadonlyArray<ts.Diagnostic>, pretty?: boolean): IterableIterator<[string, string, number]> {
diagnostics = diagnostics.slice().sort(ts.compareDiagnostics);
diagnostics = ts.sort(diagnostics, ts.compareDiagnostics);
let outputLines = "";
// Count up all errors that were found in files other than lib.d.ts so we don't miss any
let totalErrorsReportedInNonLibraryFiles = 0;

View File

@ -203,8 +203,10 @@ namespace ts.server {
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
export function combineProjectOutput<T>(projects: ReadonlyArray<Project>, action: (project: Project) => ReadonlyArray<T>, comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) {
const result = flatMap(projects, action).sort(comparer);
return projects.length > 1 ? deduplicate(result, areEqual) : result;
const outputs = flatMap(projects, action);
return comparer
? sortAndDeduplicate(outputs, comparer, areEqual)
: deduplicate(outputs, areEqual);
}
export interface HostConfiguration {

View File

@ -250,7 +250,7 @@ namespace ts.server {
return;
}
const insertIndex = binarySearch(array, insert, compare);
const insertIndex = binarySearch(array, insert, identity, compare);
if (insertIndex < 0) {
array.splice(~insertIndex, 0, insert);
}
@ -266,7 +266,7 @@ namespace ts.server {
return;
}
const removeIndex = binarySearch(array, remove, compare);
const removeIndex = binarySearch(array, remove, identity, compare);
if (removeIndex >= 0) {
array.splice(removeIndex, 1);
}

View File

@ -581,7 +581,7 @@ namespace ts.textChanges {
return applyFormatting(nonformattedText, sourceFile, initialIndentation, delta, this.rulesProvider);
}
private static normalize(changes: Change[]): Change[] {
private static normalize(changes: Change[]) {
// order changes by start position
const normalized = stableSort(changes, (a, b) => a.range.pos - b.range.pos);
// verify that change intervals do not overlap, except possibly at end points.