diff --git a/src/commands/cmd_bit.cc b/src/commands/cmd_bit.cc index 13be25f3134..26c00285233 100644 --- a/src/commands/cmd_bit.cc +++ b/src/commands/cmd_bit.cc @@ -225,6 +225,14 @@ class CommandBitOp : public Commander { op_flag_ = kBitOpXor; else if (opname == "not") op_flag_ = kBitOpNot; + else if (opname == "diff") + op_flag_ = kBitOpDiff; + else if (opname == "diff1") + op_flag_ = kBitOpDiff1; + else if (opname == "andor") + op_flag_ = kBitOpAndOr; + else if (opname == "one") + op_flag_ = kBitOpOne; else return {Status::RedisInvalidCmd, errInvalidSyntax}; if (op_flag_ == kBitOpNot && args.size() != 4) { diff --git a/src/types/redis_bitmap.cc b/src/types/redis_bitmap.cc index 00c8d1b3fde..24ced33caeb 100644 --- a/src/types/redis_bitmap.cc +++ b/src/types/redis_bitmap.cc @@ -554,7 +554,9 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st for (uint64_t i = 0; i < frag_numkeys; i++) { lp[i] = reinterpret_cast(fragments[i].data()); } - memcpy(frag_res.get(), fragments[0].data(), frag_minlen); + if (op_flag != kBitOpDiff && op_flag != kBitOpDiff1 && op_flag != kBitOpAndOr) { + memcpy(frag_res.get(), fragments[0].data(), frag_minlen); + } auto apply_fast_path_op = [&](auto op) { // Note: kBitOpNot cannot use this op, it only applying // to kBitOpAnd, kBitOpOr, kBitOpXor. @@ -589,14 +591,106 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st j += sizeof(uint64_t) * 4; frag_minlen -= sizeof(uint64_t) * 4; } + } else if (op_flag == kBitOpDiff || op_flag == kBitOpDiff1 || op_flag == kBitOpAndOr) { + size_t processed = 0; + size_t k = 0; + + while (frag_minlen >= sizeof(uint64_t) * 4) { + for (uint64_t i = 1; i < frag_numkeys; i++) { + lres[0] |= lp[i][k + 0]; + lres[1] |= lp[i][k + 1]; + lres[2] |= lp[i][k + 2]; + lres[3] |= lp[i][k + 3]; + } + // For Diff1, we need the OR of ALL keys (including the first) + if (op_flag == kBitOpDiff1) { + lres[0] |= lp[0][k + 0]; + lres[1] |= lp[0][k + 1]; + lres[2] |= lp[0][k + 2]; + lres[3] |= lp[0][k + 3]; + } + k += 4; + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + processed += sizeof(uint64_t) * 4; + } + + lres = reinterpret_cast(frag_res.get()); + auto *first_key = reinterpret_cast(fragments[0].data()); + switch (op_flag) { + case kBitOpDiff: + for (uint64_t i = 0; i < processed; i += sizeof(uint64_t) * 4) { + lres[0] = (first_key[0] & ~lres[0]); + lres[1] = (first_key[1] & ~lres[1]); + lres[2] = (first_key[2] & ~lres[2]); + lres[3] = (first_key[3] & ~lres[3]); + lres += 4; + first_key += 4; + } + break; + case kBitOpDiff1: + for (uint64_t i = 0; i < processed; i += sizeof(uint64_t) * 4) { + lres[0] = (~first_key[0] & lres[0]); + lres[1] = (~first_key[1] & lres[1]); + lres[2] = (~first_key[2] & lres[2]); + lres[3] = (~first_key[3] & lres[3]); + lres += 4; + first_key += 4; + } + break; + case kBitOpAndOr: + for (uint64_t i = 0; i < processed; i += sizeof(uint64_t) * 4) { + lres[0] = (first_key[0] & lres[0]); + lres[1] = (first_key[1] & lres[1]); + lres[2] = (first_key[2] & lres[2]); + lres[3] = (first_key[3] & lres[3]); + lres += 4; + first_key += 4; + } + break; + } + } else if (op_flag == kBitOpOne) { + uint64_t lcommon_bits[4]; + size_t k = 0; + + while (frag_minlen >= sizeof(uint64_t) * 4) { + memset(lcommon_bits, 0, sizeof(lcommon_bits)); + + for (size_t i = 1; i < frag_numkeys; i++) { + lcommon_bits[0] |= (lres[0] & lp[i][k + 0]); + lcommon_bits[1] |= (lres[1] & lp[i][k + 1]); + lcommon_bits[2] |= (lres[2] & lp[i][k + 2]); + lcommon_bits[3] |= (lres[3] & lp[i][k + 3]); + + lres[0] ^= lp[i][k + 0]; + lres[1] ^= lp[i][k + 1]; + lres[2] ^= lp[i][k + 2]; + lres[3] ^= lp[i][k + 3]; + } + + lres[0] &= ~lcommon_bits[0]; + lres[1] &= ~lcommon_bits[1]; + lres[2] &= ~lcommon_bits[2]; + lres[3] &= ~lcommon_bits[3]; + + k += 4; + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + } } } #endif - uint8_t output = 0, byte = 0; + uint8_t output = 0, byte = 0, disjunction = 0, common_bits = 0; for (; j < frag_maxlen; j++) { output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; if (op_flag == kBitOpNot) output = ~output; + // For Diff1, disjunction starts with the first key's value + if (op_flag == kBitOpDiff1) { + disjunction = output; + } for (uint64_t i = 1; i < frag_numkeys; i++) { byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; switch (op_flag) { @@ -609,11 +703,34 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st case kBitOpXor: output ^= byte; break; + case kBitOpDiff: + case kBitOpDiff1: + case kBitOpAndOr: + disjunction |= byte; + break; + case kBitOpOne: + common_bits |= (output & byte); + output ^= byte; + output &= ~common_bits; + break; default: break; } } - frag_res[j] = output; + switch (op_flag) { + case kBitOpDiff: + frag_res[j] = (output & ~disjunction); + break; + case kBitOpDiff1: + frag_res[j] = (~output & disjunction); + break; + case kBitOpAndOr: + frag_res[j] = (output & disjunction); + break; + default: + frag_res[j] = output; + break; + } } if (op_flag == kBitOpNot) { diff --git a/src/types/redis_bitmap.h b/src/types/redis_bitmap.h index 32a53cc72ab..6fa0cc16233 100644 --- a/src/types/redis_bitmap.h +++ b/src/types/redis_bitmap.h @@ -34,6 +34,10 @@ enum BitOpFlags { kBitOpOr, kBitOpXor, kBitOpNot, + kBitOpDiff, + kBitOpDiff1, + kBitOpAndOr, + kBitOpOne, }; namespace redis { diff --git a/tests/gocase/unit/type/bitmap/bitmap_test.go b/tests/gocase/unit/type/bitmap/bitmap_test.go index 6211ae3c836..5edc74d3247 100644 --- a/tests/gocase/unit/type/bitmap/bitmap_test.go +++ b/tests/gocase/unit/type/bitmap/bitmap_test.go @@ -271,6 +271,17 @@ func TestBitmap(t *testing.T) { require.EqualValues(t, []string{"\x55\xff\x00\xaa"}, GetBitmap(t, rdb, ctx, "s")) }) + t.Run("BITOP DIFF|DIFF1|ANDOR|ONE don't change the string with single input key", func(t *testing.T) { + Set2SetBit(t, rdb, ctx, "a", []byte("\x01\x02\xff")) + Set2SetBit(t, rdb, ctx, "b", []byte("\x01\x02\xff")) + Set2SetBit(t, rdb, ctx, "c", []byte("\x01\x02\xff")) + require.NoError(t, rdb.BitOpDiff(ctx, "res1", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpDiff1(ctx, "res2", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpAndOr(ctx, "res3", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpOne(ctx, "res4", "a", "b", "c").Err()) + require.EqualValues(t, []string{"\x00\x00\x00", "\x00\x00\x00", "\x01\x02\xff", "\x00\x00\x00"}, GetBitmap(t, rdb, ctx, "res1", "res2", "res3", "res4")) + }) + t.Run("BITOP AND|OR|XOR don't change the string with single input key", func(t *testing.T) { Set2SetBit(t, rdb, ctx, "a", []byte("\x01\x02\xff")) require.NoError(t, rdb.BitOpAnd(ctx, "res1", "a").Err()) @@ -568,4 +579,350 @@ func TestBitmap(t *testing.T) { } }) + t.Run("BITOP DIFF|DIFF1|ANDOR|ONE with long bitmaps (fast path)", func(t *testing.T) { + // Use bitmaps longer than 32 bytes to trigger fast path (sizeof(uint64_t) * 4) + Set2SetBit(t, rdb, ctx, "a", makeLongBitmap([]byte{0x01, 0x02, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "b", makeLongBitmap([]byte{0x01, 0x02, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "c", makeLongBitmap([]byte{0x01, 0x02, 0xff}, 40)) + + require.NoError(t, rdb.BitOpDiff(ctx, "res1", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpDiff1(ctx, "res2", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpAndOr(ctx, "res3", "a", "b", "c").Err()) + require.NoError(t, rdb.BitOpOne(ctx, "res4", "a", "b", "c").Err()) + + expected := make([]string, 4) + for i := range expected { + expected[i] = makeExpectedBitmap(makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + } + expected[2] = makeExpectedBitmap(makeLongBitmap([]byte{0x01, 0x02, 0xff}, 40)) // BitOpAndOr + + require.EqualValues(t, expected, GetBitmap(t, rdb, ctx, "res1", "res2", "res3", "res4")) + }) + + t.Run("BITOP DIFF1 with different long bitmaps", func(t *testing.T) { + // Test Diff1: (~first) & (all_or) + Set2SetBit(t, rdb, ctx, "a", makeLongBitmap([]byte{0xaa, 0x55, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "b", makeLongBitmap([]byte{0x55, 0xaa, 0x00}, 40)) + Set2SetBit(t, rdb, ctx, "c", makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + + require.NoError(t, rdb.BitOpDiff1(ctx, "res", "a", "b", "c").Err()) + + // ~a = 0x55aa00, all_or = 0xffaaff + // result = 0x55aa00 & 0xffaaff = 0x55aa00 + expected := makeExpectedBitmap(makeLongBitmap([]byte{0x55, 0xaa, 0x00}, 40)) + require.EqualValues(t, expected, rdb.Get(ctx, "res").Val()) + }) + + t.Run("BITOP DIFF with different long bitmaps", func(t *testing.T) { + // Test Diff: first & ~(others_or) + Set2SetBit(t, rdb, ctx, "a", makeLongBitmap([]byte{0xaa, 0x55, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "b", makeLongBitmap([]byte{0x55, 0xaa, 0x00}, 40)) + Set2SetBit(t, rdb, ctx, "c", makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + + require.NoError(t, rdb.BitOpDiff(ctx, "res", "a", "b", "c").Err()) + + // a = 0xaa55ff, others_or = 0x55aa00 + // result = 0xaa55ff & ~0x55aa00 = 0xaa55ff & 0xaa55ff = 0xaa55ff + expected := makeExpectedBitmap(makeLongBitmap([]byte{0xaa, 0x55, 0xff}, 40)) + require.EqualValues(t, expected, rdb.Get(ctx, "res").Val()) + }) + + t.Run("BITOP AndOr with different long bitmaps", func(t *testing.T) { + // Test AndOr: first & (others_or) + Set2SetBit(t, rdb, ctx, "a", makeLongBitmap([]byte{0xaa, 0x55, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "b", makeLongBitmap([]byte{0x55, 0xaa, 0x00}, 40)) + Set2SetBit(t, rdb, ctx, "c", makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + + require.NoError(t, rdb.BitOpAndOr(ctx, "res", "a", "b", "c").Err()) + + // First byte: b|c = 0x55|0x00 = 0x55, a & (b|c) = 0xaa & 0x55 = 0x00 + // Second byte: b|c = 0xaa|0x00 = 0xaa, a & (b|c) = 0x55 & 0xaa = 0x00 + // Third byte: b|c = 0x00|0x00 = 0x00, a & (b|c) = 0xff & 0x00 = 0x00 + expected := makeExpectedBitmap(makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + require.EqualValues(t, expected, rdb.Get(ctx, "res").Val()) + }) + + t.Run("BITOP One with different long bitmaps", func(t *testing.T) { + // Test One: (a XOR b XOR c) AND ~(a AND b AND c) + Set2SetBit(t, rdb, ctx, "a", makeLongBitmap([]byte{0xaa, 0x55, 0xff}, 40)) + Set2SetBit(t, rdb, ctx, "b", makeLongBitmap([]byte{0x55, 0xaa, 0x00}, 40)) + Set2SetBit(t, rdb, ctx, "c", makeLongBitmap([]byte{0x00, 0x00, 0x00}, 40)) + + require.NoError(t, rdb.BitOpOne(ctx, "res", "a", "b", "c").Err()) + + // First byte: 0xaa ^ 0x55 ^ 0x00 = 0xff, 0xaa & 0x55 & 0x00 = 0x00, result = 0xff & ~0x00 = 0xff + // Second byte: 0x55 ^ 0xaa ^ 0x00 = 0xff, 0x55 & 0xaa & 0x00 = 0x00, result = 0xff & ~0x00 = 0xff + // Third byte: 0xff ^ 0x00 ^ 0x00 = 0xff, 0xff & 0x00 & 0x00 = 0x00, result = 0xff & ~0x00 = 0xff + expected := makeExpectedBitmap(makeLongBitmap([]byte{0xff, 0xff, 0xff}, 40)) + require.EqualValues(t, expected, rdb.Get(ctx, "res").Val()) + }) + + t.Run("BITOP DIFF1 fuzzing with long bitmaps", func(t *testing.T) { + for i := 0; i < 5; i++ { + require.NoError(t, rdb.FlushAll(ctx).Err()) + numVec := util.RandomInt(5) + 2 + var vec [][]byte + var veckeys []string + for j := 0; j < int(numVec); j++ { + // Generate long bitmaps (>32 bytes) to trigger fast path + str := util.RandString(33, 100, util.Binary) + vec = append(vec, []byte(str)) + veckeys = append(veckeys, "vector_"+strconv.Itoa(j)) + Set2SetBit(t, rdb, ctx, "vector_"+strconv.Itoa(j), []byte(str)) + } + + // Test Diff1 + require.NoError(t, rdb.BitOpDiff1(ctx, "target", veckeys...).Err()) + expected := SimulateBitOpDiff1(vec) + require.EqualValues(t, expected, rdb.Get(ctx, "target").Val()) + } + }) + + t.Run("BITOP DIFF fuzzing with long bitmaps", func(t *testing.T) { + for i := 0; i < 5; i++ { + require.NoError(t, rdb.FlushAll(ctx).Err()) + numVec := util.RandomInt(5) + 2 + var vec [][]byte + var veckeys []string + for j := 0; j < int(numVec); j++ { + // Generate long bitmaps (>32 bytes) to trigger fast path + str := util.RandString(33, 100, util.Binary) + vec = append(vec, []byte(str)) + veckeys = append(veckeys, "vector_"+strconv.Itoa(j)) + Set2SetBit(t, rdb, ctx, "vector_"+strconv.Itoa(j), []byte(str)) + } + + // Test Diff + require.NoError(t, rdb.BitOpDiff(ctx, "target", veckeys...).Err()) + expected := SimulateBitOpDiff(vec) + require.EqualValues(t, expected, rdb.Get(ctx, "target").Val()) + } + }) + + t.Run("BITOP AndOr fuzzing with long bitmaps", func(t *testing.T) { + for i := 0; i < 5; i++ { + require.NoError(t, rdb.FlushAll(ctx).Err()) + numVec := util.RandomInt(5) + 2 + var vec [][]byte + var veckeys []string + for j := 0; j < int(numVec); j++ { + // Generate long bitmaps (>32 bytes) to trigger fast path + str := util.RandString(33, 100, util.Binary) + vec = append(vec, []byte(str)) + veckeys = append(veckeys, "vector_"+strconv.Itoa(j)) + Set2SetBit(t, rdb, ctx, "vector_"+strconv.Itoa(j), []byte(str)) + } + + // Test AndOr + require.NoError(t, rdb.BitOpAndOr(ctx, "target", veckeys...).Err()) + expected := SimulateBitOpAndOr(vec) + require.EqualValues(t, expected, rdb.Get(ctx, "target").Val()) + } + }) + + t.Run("BITOP One fuzzing with long bitmaps", func(t *testing.T) { + for i := 0; i < 5; i++ { + require.NoError(t, rdb.FlushAll(ctx).Err()) + numVec := util.RandomInt(5) + 2 + var vec [][]byte + var veckeys []string + for j := 0; j < int(numVec); j++ { + // Generate long bitmaps (>32 bytes) to trigger fast path + str := util.RandString(33, 100, util.Binary) + vec = append(vec, []byte(str)) + veckeys = append(veckeys, "vector_"+strconv.Itoa(j)) + Set2SetBit(t, rdb, ctx, "vector_"+strconv.Itoa(j), []byte(str)) + } + + // Test One + require.NoError(t, rdb.BitOpOne(ctx, "target", veckeys...).Err()) + expected := SimulateBitOpOne(vec) + require.EqualValues(t, expected, rdb.Get(ctx, "target").Val()) + } + }) +} + +// Helper functions for testing fast path + +// makeLongBitmap creates a long bitmap with pattern as prefix and zeros as padding +func makeLongBitmap(pattern []byte, length int) []byte { + result := make([]byte, length) + for i := 0; i < length; i++ { + if i < len(pattern) { + result[i] = pattern[i] + } else { + result[i] = 0 + } + } + return result +} + +// makeExpectedBitmap creates expected string from a byte slice +func makeExpectedBitmap(data []byte) string { + return string(data) } + +// SimulateBitOpDiff1 simulates BitOpDiff1: (~first) & (all_or) +func SimulateBitOpDiff1(vec [][]byte) string { + if len(vec) == 0 { + return "" + } + + // Find max length + maxlen := 0 + for _, v := range vec { + if len(v) > maxlen { + maxlen = len(v) + } + } + + // Calculate all_or + allOr := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + for j := range vec { + if i < len(vec[j]) { + allOr[i] |= vec[j][i] + } + } + } + + // Calculate (~first) & allOr + result := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + if i < len(vec[0]) { + result[i] = (^vec[0][i]) & allOr[i] + } else { + result[i] = allOr[i] + } + } + + return string(result) +} + +// SimulateBitOpDiff simulates BitOpDiff: first & ~(others_or) +func SimulateBitOpDiff(vec [][]byte) string { + if len(vec) == 0 { + return "" + } + + // Find max length + maxlen := 0 + for _, v := range vec { + if len(v) > maxlen { + maxlen = len(v) + } + } + + // Calculate others_or + othersOr := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + for j := 1; j < len(vec); j++ { + if i < len(vec[j]) { + othersOr[i] |= vec[j][i] + } + } + } + + // Calculate first & ~othersOr + result := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + if i < len(vec[0]) { + result[i] = vec[0][i] & (^othersOr[i]) + } else { + result[i] = 0 + } + } + + return string(result) +} + +// SimulateBitOpAndOr simulates BitOpAndOr: first & (others_or) +func SimulateBitOpAndOr(vec [][]byte) string { + if len(vec) == 0 { + return "" + } + + // Find max length + maxlen := 0 + for _, v := range vec { + if len(v) > maxlen { + maxlen = len(v) + } + } + + // Calculate others_or + othersOr := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + for j := 1; j < len(vec); j++ { + if i < len(vec[j]) { + othersOr[i] |= vec[j][i] + } + } + } + + // Calculate first & othersOr + result := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + if i < len(vec[0]) { + result[i] = vec[0][i] & othersOr[i] + } else { + result[i] = 0 + } + } + + return string(result) +} + +// SimulateBitOpOne simulates BitOpOne: (all_xor) & ~(all_and) +func SimulateBitOpOne(vec [][]byte) string { + if len(vec) == 0 { + return "" + } + + // Find max length + maxlen := 0 + for _, v := range vec { + if len(v) > maxlen { + maxlen = len(v) + } + } + + // Calculate all_xor + allXor := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + for j := range vec { + if i < len(vec[j]) { + allXor[i] ^= vec[j][i] + } + } + } + + // Calculate all_and + allAnd := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + // Initialize with the first key's value or 0 if out of bounds + if i < len(vec[0]) { + allAnd[i] = vec[0][i] + } else { + allAnd[i] = 0 + } + // AND with all other keys + for j := 1; j < len(vec); j++ { + if i < len(vec[j]) { + allAnd[i] &= vec[j][i] + } else { + // If key j is shorter at this position, treat it as 0 + allAnd[i] &= 0 + } + } + } + + // Calculate all_xor & ~all_and + result := make([]byte, maxlen) + for i := 0; i < maxlen; i++ { + result[i] = allXor[i] & (^allAnd[i]) + } + + return string(result) +} +