mirror of
https://github.com/metowolf/iplist.git
synced 2026-11-05 22:56:43 +08:00
perf: 优化 CIDR 合并并补充前后性能基准 (#37)
This commit is contained in:
3 files changed
+108
-20
No files matched your search
+63
-6
@@ -2,7 +2,18 @@
|
||||
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { merge } = require('../src/lib/cidr')
|
||||
const assert = require('node:assert/strict')
|
||||
const { parseArgs } = require('node:util')
|
||||
|
||||
const { values } = parseArgs({ options: {
|
||||
baseline: { type: 'string' },
|
||||
candidate: { type: 'string' },
|
||||
extended: { type: 'boolean', default: false },
|
||||
json: { type: 'string' },
|
||||
filter: { type: 'string' }
|
||||
} })
|
||||
const baseline = values.baseline ? require(path.resolve(values.baseline)).merge : null
|
||||
const { merge } = require(values.candidate ? path.resolve(values.candidate) : '../src/lib/cidr')
|
||||
|
||||
const DATA = path.join(__dirname, '..', 'data')
|
||||
const readLines = (file) => fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean)
|
||||
@@ -17,22 +28,43 @@ function loadCases () {
|
||||
|
||||
const cncity = fs.readdirSync(path.join(DATA, 'cncity')).map((f) => readLines(path.join(DATA, 'cncity', f)))
|
||||
|
||||
return [
|
||||
const cases = [
|
||||
{ name: 'CN.txt', inputs: [readLines(path.join(DATA, 'country', 'CN.txt'))] },
|
||||
{ name: 'non-china (all countries except CN)', inputs: [nonChina] },
|
||||
{ name: `cncity/*.txt (${cncity.length} files)`, inputs: cncity },
|
||||
{ name: `every data/**/*.txt separately (${files.length} files)`, inputs: files.map(readLines) },
|
||||
{ name: 'all data/**/*.txt concatenated', inputs: [everything] }
|
||||
]
|
||||
if (values.extended) {
|
||||
const ascending = merge(everything, true)
|
||||
const shuffled = ascending.slice()
|
||||
let seed = 42
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
||||
const j = seed % (i + 1)
|
||||
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
||||
}
|
||||
const v6 = Array.from({ length: 10000 }, (_, i) => `2001:db8:${i.toString(16)}::1/128`)
|
||||
const v6Networks = Array.from({ length: 10000 }, (_, i) => `2001:db8:${i.toString(16)}::/64`)
|
||||
cases.push(
|
||||
{ name: 'IPv4 ascending', inputs: [ascending] },
|
||||
{ name: 'IPv4 descending', inputs: [ascending.slice().reverse()] },
|
||||
{ name: 'IPv4 shuffled (seed 42)', inputs: [shuffled] },
|
||||
{ name: 'IPv6 synthetic 10000 /128', inputs: [v6] },
|
||||
{ name: 'IPv6 synthetic 10000 /128 sort=false', inputs: [v6], sort: false },
|
||||
{ name: 'IPv6 synthetic 10000 /64', inputs: [v6Networks] },
|
||||
{ name: 'mixed synthetic sort=false', inputs: [ascending.slice(0, 10000).concat(v6)], sort: false }
|
||||
)
|
||||
}
|
||||
return cases.filter((c) => !values.filter || c.name.includes(values.filter))
|
||||
}
|
||||
|
||||
function bench (fn, inputs, minMs = 1500) {
|
||||
for (let i = 0; i < 3; i++) for (const input of inputs) fn(input, true) // warm-up
|
||||
function bench (fn, inputs, sort, minMs) {
|
||||
let iters = 0
|
||||
const t0 = process.hrtime.bigint()
|
||||
let elapsed = 0
|
||||
while (elapsed < minMs || iters < 5) {
|
||||
for (const input of inputs) fn(input, true)
|
||||
for (const input of inputs) fn(input, sort)
|
||||
iters++
|
||||
elapsed = Number(process.hrtime.bigint() - t0) / 1e6
|
||||
}
|
||||
@@ -40,17 +72,42 @@ function bench (fn, inputs, minMs = 1500) {
|
||||
}
|
||||
|
||||
const fmt = (ms) => (ms < 1 ? `${(ms * 1000).toFixed(1)} µs` : `${ms.toFixed(2)} ms`)
|
||||
const median = (samples) => samples.slice().sort((a, b) => a - b)[Math.floor(samples.length / 2)]
|
||||
|
||||
console.log(`node ${process.version}\n`)
|
||||
const rows = []
|
||||
const results = []
|
||||
for (const c of loadCases()) {
|
||||
const size = c.inputs.reduce((n, a) => n + a.length, 0)
|
||||
const b = bench(merge, c.inputs)
|
||||
const sort = c.sort !== false
|
||||
for (const input of c.inputs) {
|
||||
if (baseline) assert.deepEqual(merge(input, sort), baseline(input, sort), c.name)
|
||||
}
|
||||
const before = []
|
||||
const after = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (const input of c.inputs) {
|
||||
if (baseline) baseline(input, sort)
|
||||
merge(input, sort)
|
||||
}
|
||||
}
|
||||
// Alternate execution order to reduce drift; report the median of five batches.
|
||||
for (let round = 0; round < (baseline ? 5 : 1); round++) {
|
||||
const runBefore = () => before.push(bench(baseline, c.inputs, sort, 300))
|
||||
const runAfter = () => after.push(bench(merge, c.inputs, sort, baseline ? 300 : 1500))
|
||||
if (baseline && round % 2 === 0) runBefore()
|
||||
runAfter()
|
||||
if (baseline && round % 2 !== 0) runBefore()
|
||||
}
|
||||
const b = median(after)
|
||||
results.push({ name: c.name, size, sort, before, after, beforeMs: baseline ? median(before) : null, afterMs: b })
|
||||
rows.push({
|
||||
case: c.name,
|
||||
'input CIDRs': size,
|
||||
...(baseline ? { baseline: fmt(median(before)), speedup: `${(median(before) / b).toFixed(2)}x` } : {}),
|
||||
'src/lib/cidr': fmt(b),
|
||||
'CIDRs / second': Math.round(size / (b / 1000)).toLocaleString('en-US')
|
||||
})
|
||||
}
|
||||
console.table(rows)
|
||||
if (values.json) fs.writeFileSync(values.json, JSON.stringify({ node: process.version, results }, null, 2) + '\n')
|
||||
+23
-14
@@ -46,7 +46,7 @@ function parseV4Packed (s) {
|
||||
if (++digits > 3) return bad(s)
|
||||
} else if (c === 46) { // '.'
|
||||
if (digits === 0 || octet > 255 || ++dots > 3) return bad(s)
|
||||
ip = ip * 256 + octet
|
||||
ip = (ip << 8) | octet
|
||||
octet = 0
|
||||
digits = 0
|
||||
} else if (c === 47) { // '/'
|
||||
@@ -58,7 +58,7 @@ function parseV4Packed (s) {
|
||||
}
|
||||
}
|
||||
if (digits === 0 || octet > 255 || dots !== 3) return bad(s)
|
||||
ip = ip * 256 + octet
|
||||
ip = (ip << 8) | octet
|
||||
|
||||
let prefix = 32
|
||||
if (i < len) {
|
||||
@@ -74,7 +74,9 @@ function parseV4Packed (s) {
|
||||
if (prefix > 32) return bad(s)
|
||||
}
|
||||
const hb = 32 - prefix
|
||||
const start = ip - (ip % POW2[hb])
|
||||
const ip32 = ip >>> 0
|
||||
// JS masks shift counts to five bits, so /0 needs an explicit case.
|
||||
const start = hb === 32 ? 0 : ((ip32 >>> hb) << hb) >>> 0
|
||||
return start * 64 + hb
|
||||
}
|
||||
|
||||
@@ -95,20 +97,21 @@ function emitV4 (s, e, out, starts) {
|
||||
}
|
||||
}
|
||||
|
||||
function mergeV4 (keys, count, out, starts) {
|
||||
function mergeV4 (keys, count, out, starts, ordered) {
|
||||
if (count === 0) return
|
||||
const sorted = count === keys.length ? keys : keys.subarray(0, count)
|
||||
sorted.sort()
|
||||
// Project data files are already ordered by normalized packed key.
|
||||
if (!ordered) sorted.sort()
|
||||
|
||||
let k = sorted[0]
|
||||
let hb = k % 64
|
||||
let curS = (k - hb) / 64
|
||||
let curS = Math.floor(k / 64)
|
||||
let hb = k - curS * 64
|
||||
let curE = curS + POW2[hb] - 1
|
||||
|
||||
for (let i = 1; i < count; i++) {
|
||||
k = sorted[i]
|
||||
hb = k % 64
|
||||
const s = (k - hb) / 64
|
||||
const s = Math.floor(k / 64)
|
||||
hb = k - s * 64
|
||||
if (s > curE + 1) {
|
||||
emitV4(curS, curE, out, starts)
|
||||
curS = s
|
||||
@@ -221,7 +224,7 @@ function emitV6 (s, e, out, starts) {
|
||||
const hbAlign = s === 0n ? 128 : bitLength(s & -s) - 1
|
||||
const hb = hbLen < hbAlign ? hbLen : hbAlign
|
||||
out.push(v6ToString(s, 128 - hb))
|
||||
starts.push(s)
|
||||
if (starts !== null) starts.push(s)
|
||||
s += 1n << BigInt(hb)
|
||||
}
|
||||
}
|
||||
@@ -255,6 +258,8 @@ function merge (cidrs, sort = false) {
|
||||
const len = cidrs.length
|
||||
const keys = new Float64Array(len)
|
||||
let count = 0
|
||||
let ordered = true
|
||||
let previous = -1
|
||||
let v6 = null
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
@@ -264,20 +269,24 @@ function merge (cidrs, sort = false) {
|
||||
if (v6 === null) v6 = []
|
||||
v6.push(parseV6(c))
|
||||
} else {
|
||||
if (k < previous) ordered = false
|
||||
previous = k
|
||||
keys[count++] = k
|
||||
}
|
||||
}
|
||||
|
||||
const out4 = []
|
||||
if (v6 === null) {
|
||||
mergeV4(keys, count, out4, null)
|
||||
mergeV4(keys, count, out4, null, ordered)
|
||||
return out4
|
||||
}
|
||||
|
||||
const starts4 = []
|
||||
// Start arrays are only needed to interleave the two address families.
|
||||
const trackStarts = sort && count !== 0
|
||||
const starts4 = trackStarts ? [] : null
|
||||
const out6 = []
|
||||
const starts6 = []
|
||||
mergeV4(keys, count, out4, starts4)
|
||||
const starts6 = trackStarts ? [] : null
|
||||
mergeV4(keys, count, out4, starts4, ordered)
|
||||
mergeV6(v6, out6, starts6)
|
||||
|
||||
if (!sort || out4.length === 0 || out6.length === 0) return out4.concat(out6)
|
||||
|
||||
@@ -79,6 +79,28 @@ test('edge cases IPv4', () => {
|
||||
check([])
|
||||
})
|
||||
|
||||
test('IPv4 bit masks cover every prefix and unsigned boundary', () => {
|
||||
for (let prefix = 0; prefix <= 32; prefix++) {
|
||||
for (const ip of ['0.0.0.0', '0.0.0.1', '127.255.255.255', '128.0.0.0', '255.255.255.255']) {
|
||||
check([`${ip}/${prefix}`])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('IPv4 ordering detection uses normalized packed keys across IPv6 entries', () => {
|
||||
const inputs = [
|
||||
['10.0.0.255/24', '10.0.0.1/32', '10.0.0.1/32', '10.0.2.0/24'],
|
||||
['10.0.0.1/32', '10.0.0.255/24', '10.0.2.0/24'],
|
||||
['255.255.255.255', '128.0.0.0/1', '0.0.0.0/1'],
|
||||
['0.0.0.1', '::1', '0.0.0.2', '::2'],
|
||||
['0.0.0.2', '::1', '0.0.0.1', '::2']
|
||||
]
|
||||
for (const input of inputs) {
|
||||
check(input, true)
|
||||
check(input, false)
|
||||
}
|
||||
})
|
||||
|
||||
test('edge cases IPv6 and mixed', () => {
|
||||
check(['::/0'])
|
||||
check(['::1', '::'])
|
||||
|
||||
Reference in new issue
Block a user