Concrete
WebsiteLibrariesProducts & ServicesDevelopersSupport
2.5
2.5
  • What is Concrete?
  • Getting Started
    • Basics of FHE programs
    • Installation
    • Quick Start
    • Compatibility
    • Exactness
    • Performance
    • Terminology and Structure
  • Tutorials
    • Decorator
    • Progressbar
    • Formatting
    • Tagging
    • Extensions
    • Comparisons
    • Min/Max Operations
    • Bitwise Operations
    • Table Lookups
    • Bit Extraction
    • Truncating
    • Rounding
    • Floating Points
    • Multi Precision
    • Multi Parameters
    • Simulation
    • Direct Circuits
    • Statistics
    • Common Workarounds
    • Composition
  • Application Tutorials
    • Key Value Database
    • SHA-256
    • Game of Life
  • How To
    • Configure
    • Manage Keys
    • Deploy
    • Reuse Arguments
    • Debug
    • Call FHE circuits from other languages
  • Explanations
    • Frontend fusing
    • Compilation
      • Automatic Crypto Parameters choice
      • MLIR FHE Dialects
        • FHELinalg Dialect
        • FHE Dialect
        • TFHE Dialect
        • Concrete Dialect
        • Tracing Dialect
        • Runtime Dialect
        • SDFG Dialect
    • Security curves
  • Developer
    • Contribute
    • Project layout
    • Compiler backend
      • Adding a new backend
Powered by GitBook

Libraries

  • TFHE-rs
  • Concrete
  • Concrete ML
  • fhEVM

Developers

  • Blog
  • Documentation
  • Github
  • FHE resources

Company

  • About
  • Introduction to FHE
  • Media
  • Careers
On this page
  • Limitations
  • Performance Considerations
  • A Chain of Individual Bit Extractions
  • Reuse of Intermediate Extracted Bits
  • TLUs of 1b input precision

Was this helpful?

Export as PDF
  1. Tutorials

Bit Extraction

Some applications require directly manipulating bits of integers. Concrete provides a bit extraction operation for such applications.

Bit extraction is capable of extracting a slice of bits from an integer. Index 0 corresponds to the lowest significant bit. The cost of this operation is proportional to the highest significant bit index.

Bit extraction only works in the Native encoding, which is usually selected when all table lookups in the circuit are less than or equal to 8 bits.

from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.bits(x)[0], fhe.bits(x)[3]

inputset = range(32)
circuit = f.compile(inputset)

assert circuit.encrypt_run_decrypt(0b_00000) == (0, 0)
assert circuit.encrypt_run_decrypt(0b_00001) == (1, 0)

assert circuit.encrypt_run_decrypt(0b_01100) == (0, 1)
assert circuit.encrypt_run_decrypt(0b_01101) == (1, 1)

Slices can be used for indexing fhe.bits(value) as well.

from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.bits(x)[1:4]

inputset = range(32)
circuit = f.compile(inputset)

assert circuit.encrypt_run_decrypt(0b_01101) == 0b_110
assert circuit.encrypt_run_decrypt(0b_01011) == 0b_101

Even slices with negative steps are supported!

from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.bits(x)[3:0:-1]

inputset = range(32)
circuit = f.compile(inputset)

assert circuit.encrypt_run_decrypt(0b_01101) == 0b_011
assert circuit.encrypt_run_decrypt(0b_01011) == 0b_101

Signed integers are supported as well.

from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.bits(x)[1:3]

inputset = range(-16, 16)
circuit = f.compile(inputset)

assert circuit.encrypt_run_decrypt(-14) == 0b_01  # -14 == 0b_10010 (in two's complement)
assert circuit.encrypt_run_decrypt(-12) == 0b_10  # -12 == 0b_10100 (in two's complement)

Lastly, here is a practical use case of bit extraction.

import numpy as np
from concrete import fhe

@fhe.compiler({"x": "encrypted"})
def is_even(x):
    return 1 - fhe.bits(x)[0]

inputset = [
    np.random.randint(-16, 16, size=(5,))
    for _ in range(100)
]
circuit = is_even.compile(inputset)

sample = np.random.randint(-16, 16, size=(5,))
for value, value_is_even in zip(sample, circuit.encrypt_run_decrypt(sample)):
    print(f"{value} is {'even' if value_is_even else 'odd'}")

prints

13 is odd
0 is even
-15 is odd
2 is even
-6 is even

Limitations

  • Bits cannot be extracted using a negative index.

    • Which means fhe.bits(x)[-1] or fhe.bits(x)[-4:-1] is not supported for example.

    • The reason for this is that we don't know in advance (i.e., before inputset evaluation) how many bits x has.

      • For example, let's say you have x == 10 == 0b_000...0001010, and you want to do fhe.bits(x)[-1]. If the value is 4-bits (i.e., 0b_1010), the result needs to be 1, but if it's 6-bits (i.e., 0b_001010), the result needs to be 0. Since we don't know the bit-width of x before inputset evaluation, we cannot calculate fhe.bits(x)[-1].

  • When extracting bits using slices in reverse order (i.e., step < 0), the start bit needs to be provided explicitly.

    • Which means fhe.bits(x)[::-1] or fhe.bits(x)[:2:-1] is not supported for example.

    • The reason is the same as above.

  • When extracting bits of signed values using slices, the stop bit needs to be provided explicitly.

    • Which means fhe.bits(x)[1:] or fhe.bits(x)[1::2] is not supported for example.

    • The reason is similar to above.

  • Bits of floats cannot be extracted.

    • Floats are partially supported but extracting their bits is not supported at all.

Performance Considerations

A Chain of Individual Bit Extractions

Key Concept: Extracting a specific bit requires clearing all the preceding lower bits. This involves extracting these previous bits as intermediate values and then subtracting them from the input.

Implications:

  • Bits are extracted sequentially, starting from the least significant bit to the more significant ones. The cost is proportional to the index of the highest extracted bit plus one.

  • No parallelization is possible. The computation time is proportional to the cost, independent of the number of CPUs.

Examples:

  • Extracting fhe.bits(x)[4] is approximately five times costlier than extracting fhe.bits(x)[0].

  • Extracting fhe.bits(x)[4] takes around five times more wall clock time than fhe.bits(x)[0].

  • The cost of extracting fhe.bits(x)[0:5] is almost the same as that of fhe.bits(x)[5].

Reuse of Intermediate Extracted Bits

Key Concept: Common sub-expression elimination is applied to intermediate extracted bits.

Implications:

  • The overall cost for a series of fhe.bits(x)[m:n] calls on the same input x is almost equivalent to the cost of the single most computationally expensive extraction in the series, i.e. fhe.bits(x)[n].

  • The order of extraction in that series does not affect the overall cost.

Example:

The combined operation fhe.bit(x)[3] + fhe.bit(x)[2] + fhe.bit(x)[1] has almost the same cost as fhe.bits(x)[3].

TLUs of 1b input precision

Each extracted bit incurs a cost of approximately one TLU of 1-bit input precision. Therefore, fhe.bits(x)[0] is generally faster than any other TLU operation.

PreviousTable LookupsNextTruncating

Last updated 1 year ago

Was this helpful?

To explain a bit more, signed integers use representation. In this representation, negative values have their most significant bits set to 1 (e.g., -1 == 0b_11111, -2 == 0b_11110, -3 == 0b_11101). Extracting bits always returns a positive value (e.g., fhe.bits(-1)[1:3] == 0b_11 == 3) This means if you were to do fhe.bits(x)[1:] where x == -1, if x is 4 bits, the result would be 0b_111 == 7, but if x is 5 bits the result would be 0b_1111 == 15. Since we don't know the bit-width of x before inputset evaluation, we cannot calculate fhe.bits(x)[1:].

two's complement