Skip to content

Commit

Permalink
Reduce number of allocations
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbailey committed Jan 26, 2025
1 parent a6b8fde commit cf45d8e
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dev.andrewbailey.diff.impl.MyersDiffAlgorithm
import dev.andrewbailey.diff.impl.MyersDiffOperation.Delete
import dev.andrewbailey.diff.impl.MyersDiffOperation.Insert
import dev.andrewbailey.diff.impl.MyersDiffOperation.Skip
import dev.andrewbailey.diff.impl.fastForEach

internal object DiffGenerator {

Expand All @@ -18,14 +19,12 @@ internal object DiffGenerator {
updated: List<T>,
detectMoves: Boolean
): DiffResult<T> {
val diff = MyersDiffAlgorithm(original, updated)
.generateDiff()
val diff = MyersDiffAlgorithm(original, updated).generateDiff()

var index = 0
var indexInOriginalSequence = 0
val operations = mutableListOf<DiffOperation<T>>()

diff.forEach { operation ->
diff.fastForEach { operation ->
when (operation) {
is Insert -> {
operations += Add(
Expand All @@ -48,13 +47,9 @@ internal object DiffGenerator {
}
}

if (detectMoves) {
reduceDeletesAndAddsToMoves(operations)
}

return DiffResult(
operations = reduceSequences(operations)
)
if (detectMoves) reduceDeletesAndAddsToMoves(operations)
reduceSequences(operations)
return DiffResult(operations)
}

/**
Expand Down Expand Up @@ -82,10 +77,6 @@ internal object DiffGenerator {
while (index < operations.size) {
val operation = operations[index]

check(operation is Add<T> || operation is Remove<T>) {
"Only add and remove operations should appear in the diff"
}

var indexOfOppositeAction = index + 1
var endIndexDifference = 0

Expand Down Expand Up @@ -137,10 +128,7 @@ internal object DiffGenerator {
else -> false
}

private fun <T> reduceSequences(
operations: MutableList<DiffOperation<T>>
): List<DiffOperation<T>> {
val result = mutableListOf<DiffOperation<T>>()
private fun <T> reduceSequences(operations: MutableList<DiffOperation<T>>) {
var index = 0

while (index < operations.size) {
Expand All @@ -154,62 +142,49 @@ internal object DiffGenerator {
sequenceLength++
}

result += reduceSequence(
operations = operations,
sequenceStartIndex = index,
sequenceEndIndex = sequenceEndIndex
)
if (sequenceLength > 1) {
operations[index] = reduceSequence(
operations = operations,
sequenceStartIndex = index,
sequenceLength = sequenceLength
)

index += sequenceLength
}
repeat(sequenceLength - 1) { operations.removeAt(index + 1) }
}

return result
index++
}
}

private fun <T> reduceSequence(
operations: MutableList<DiffOperation<T>>,
sequenceStartIndex: Int,
sequenceEndIndex: Int
): DiffOperation<T> {
val sequenceLength = sequenceEndIndex - sequenceStartIndex
return if (sequenceLength == 1) {
operations[sequenceStartIndex]
} else {
when (val startOperation = operations[sequenceStartIndex]) {
is Remove -> {
RemoveRange(
startIndex = startOperation.index,
endIndex = startOperation.index + sequenceLength
)
}
is Add -> {
AddAll(
index = startOperation.index,
items = operations.subList(sequenceStartIndex, sequenceEndIndex)
.asSequence()
.map { operation ->
require(operation is Add<T>) {
"Cannot reduce $operation as part of an insert sequence " +
"because it is not an add action."
}

operation.item
}
.toList()
)
}
is Move -> {
MoveRange(
fromIndex = startOperation.fromIndex,
toIndex = startOperation.toIndex,
itemCount = sequenceLength
)
sequenceLength: Int
): DiffOperation<T> = when (val startOperation = operations[sequenceStartIndex]) {
is Remove -> {
RemoveRange(
startIndex = startOperation.index,
endIndex = startOperation.index + sequenceLength
)
}
is Add -> {
AddAll(
index = startOperation.index,
items = List(sequenceLength) { i ->
(operations[sequenceStartIndex + i] as Add<T>).item
}
else -> throw IllegalArgumentException(
"Cannot reduce sequence starting with $startOperation"
)
}
)
}
is Move -> {
MoveRange(
fromIndex = startOperation.fromIndex,
toIndex = startOperation.toIndex,
itemCount = sequenceLength
)
}
else -> throw IllegalArgumentException(
"Cannot reduce sequence starting with $startOperation"
)
}

private fun <T> DiffOperation<T>.canBeCombinedWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package dev.andrewbailey.diff.impl
import kotlin.jvm.JvmInline

@JvmInline
internal value class CircularIntArray(private val array: IntArray) {
internal value class CircularIntArray(val array: IntArray) {

constructor(size: Int) : this(IntArray(size))

operator fun get(index: Int): Int = array[toInternalIndex(index)]
inline operator fun get(index: Int): Int = array[toLinearIndex(index)]

operator fun set(index: Int, value: Int) {
array[toInternalIndex(index)] = value
inline operator fun set(index: Int, value: Int) {
array[toLinearIndex(index)] = value
}

private fun toInternalIndex(index: Int): Int {
private inline fun toLinearIndex(index: Int): Int {
val moddedIndex = index % array.size
return if (moddedIndex < 0) {
moddedIndex + array.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@ package dev.andrewbailey.diff.impl

import kotlin.math.abs

internal fun Int.isEven() = abs(this) % 2 == 0
internal inline fun Int.isEven() = abs(this) % 2 == 0

internal fun Int.isOdd() = abs(this) % 2 == 1
internal inline fun Int.isOdd() = abs(this) % 2 == 1

internal fun <T> MutableList<T>.push(item: T) {
internal inline fun <T> MutableList<T>.push(item: T) {
add(item)
}

internal fun <T> MutableList<T>.pop(): T {
check(isNotEmpty()) {
"List has no items"
internal inline fun <T> MutableList<T>.pop(): T = removeAt(size - 1)

internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
@Suppress("ReplaceManualRangeWithIndicesCalls")
for (i in 0 until size) {
action(this[i])
}
}

internal inline fun <T> List<T>.fastForEachIndexed(action: (Int, T) -> Unit) {
@Suppress("ReplaceManualRangeWithIndicesCalls")
for (i in 0 until size) {
action(i, this[i])
}
return removeAt(size - 1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,23 @@ internal class MyersDiffAlgorithm<T>(
private val updated: List<T>
) {

fun generateDiff(): Sequence<MyersDiffOperation<T>> = walkSnakes()
.asSequence()
.map { (x1, y1, x2, y2) ->
when {
x1 == x2 -> Insert(value = updated[y1])
y1 == y2 -> Delete
else -> Skip
}
}

private fun walkSnakes(): List<Region> {
fun generateDiff(): List<MyersDiffOperation<T>> {
val path = findPath()
val regions = mutableListOf<MyersDiffOperation<T>>()

val regions = mutableListOf<Region>()
path.forEach { (p1, p2) ->
path.fastForEach { (p1, p2) ->
var (x1, y1) = walkDiagonal(p1, p2, regions)
val (x2, y2) = p2

val dY = y2 - y1
val dX = x2 - x1
when {
dY > dX -> {
regions += Region(x1, y1, x1, y1 + 1)
regions += interpretRegion(x1, y1, x1, y1 + 1)
y1++
}
dY < dX -> {
regions += Region(x1, y1, x1 + 1, y1)
regions += interpretRegion(x1, y1, x1 + 1, y1)
x1++
}
}
Expand All @@ -78,19 +68,30 @@ internal class MyersDiffAlgorithm<T>(
private fun walkDiagonal(
start: Point,
end: Point,
regionsOutput: MutableList<Region>
regionsOutput: MutableList<MyersDiffOperation<T>>
): Point {
var (x1, y1) = start
val (x2, y2) = end
while (x1 < x2 && y1 < y2 && original[x1] == updated[y1]) {
regionsOutput += Region(x1, y1, x1 + 1, y1 + 1)
regionsOutput += interpretRegion(x1, y1, x1 + 1, y1 + 1)
x1++
y1++
}

return Point(x1, y1)
}

private fun interpretRegion(
x1: Int,
y1: Int,
x2: Int,
y2: Int
): MyersDiffOperation<T> = when {
x1 == x2 -> Insert(value = updated[y1])
y1 == y2 -> Delete
else -> Skip
}

private fun findPath(): List<Snake> {
val snakes = mutableListOf<Snake>()
val stack = mutableListOf<Region>()
Expand Down Expand Up @@ -128,14 +129,7 @@ internal class MyersDiffAlgorithm<T>(
}
}

snakes.sortWith(object : Comparator<Snake> {
override fun compare(a: Snake, b: Snake): Int = if (a.start.x == b.start.x) {
a.start.y - b.start.y
} else {
a.start.x - b.start.x
}
})

snakes.sort()
return snakes
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package dev.andrewbailey.diff.impl

internal data class Point(
val x: Int,
val y: Int
)
import kotlin.jvm.JvmInline

@JvmInline
internal value class Point(private val packed: Long) {
val x: Int get() = (packed and 0xFFFFFFFF).toInt()
val y: Int get() = (packed shr 32).toInt()

constructor(x: Int, y: Int) : this(
(x.toLong() and 0xFFFFFFFF) or (y.toLong() shl 32)
)

inline operator fun component1() = x
inline operator fun component2() = y
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ package dev.andrewbailey.diff.impl
internal data class Snake(
val start: Point,
val end: Point
)
) : Comparable<Snake> {
override fun compareTo(other: Snake): Int = if (start.x == other.start.x) {
start.y - other.start.y
} else {
start.x - other.start.x
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class MyersDiffAlgorithmTest {
)
}

private fun <T> applyDiff(original: List<T>, diff: Sequence<MyersDiffOperation<T>>): List<T> =
private fun <T> applyDiff(original: List<T>, diff: List<MyersDiffOperation<T>>): List<T> =
original.toMutableList().apply {
var index = 0
diff.forEach { operation ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import dev.andrewbailey.diff.DiffOperation.Move
import dev.andrewbailey.diff.DiffOperation.MoveRange
import dev.andrewbailey.diff.DiffOperation.Remove
import dev.andrewbailey.diff.DiffOperation.RemoveRange
import dev.andrewbailey.diff.impl.fastForEach
import dev.andrewbailey.diff.impl.fastForEachIndexed

/**
* This class serves as a convenience class for Java users who may find it tedious to call
Expand All @@ -18,7 +20,7 @@ import dev.andrewbailey.diff.DiffOperation.RemoveRange
abstract class DiffReceiver<T> {

fun applyDiff(diff: DiffResult<T>) {
diff.operations.forEach { operation ->
diff.operations.fastForEach { operation ->
when (operation) {
is Remove -> {
remove(operation.index)
Expand Down Expand Up @@ -53,7 +55,7 @@ abstract class DiffReceiver<T> {
abstract fun insert(item: T, index: Int)

open fun insertAll(items: List<T>, index: Int) {
items.forEachIndexed { itemIndex, item ->
items.fastForEachIndexed { itemIndex, item ->
insert(item, index + itemIndex)
}
}
Expand Down

0 comments on commit cf45d8e

Please sign in to comment.