Performance

This document provides a detailed analysis of the performance characteristics between ldbc and jdbc, helping you understand when and why to choose ldbc based on technical considerations.

Executive Summary

Benchmark results show that ldbc demonstrates approximately 1.8-2.1x higher throughput compared to jdbc. This advantage stems from Cats Effect's Fiber-based concurrency model and non-blocking I/O implementation. ldbc particularly excels in high-concurrency environments with superior scalability.

Key Findings

  1. Performance: ldbc is ~2x faster than jdbc (8-thread environment)
  2. Scalability: ldbc achieves near-linear scaling with increased thread count
  3. Resource Efficiency: Significantly reduced memory usage (Fiber: 500 bytes vs OS Thread: 1MB)
  4. Latency: Maintains stable response times even under high load

Benchmark Environment

Hardware and Software Environment

JMH Benchmark Configuration

@BenchmarkMode(Array(Mode.Throughput))      // Throughput measurement
@OutputTimeUnit(TimeUnit.SECONDS)           // Output in ops/s
@State(Scope.Benchmark)                     // Benchmark scope
@Fork(value = 1)                            // Fork count: 1
@Warmup(iterations = 5)                     // Warmup: 5 iterations
@Measurement(iterations = 10)               // Measurement: 10 iterations
@Threads(1)                                 // Thread count: varies 1-16

Test Conditions

ldbc-specific Configuration

MySQLDataSource
  .build[IO]("127.0.0.1", 13306, "ldbc")
  .setPassword("password")
  .setDatabase("benchmark")
  .setSSL(SSL.Trusted)
  // Default settings (no optimization)
  .setUseServerPrepStmts(false)
  .setUseCursorFetch(false)

jdbc-specific Configuration

val ds = new MysqlDataSource()
ds.setServerName("127.0.0.1")
ds.setPortNumber(13306)
ds.setDatabaseName("benchmark")
ds.setUser("ldbc")
ds.setPassword("password")
ds.setUseSSL(true)
// Fixed-size thread pool
val executorService = Executors.newFixedThreadPool(
  Math.max(4, Runtime.getRuntime.availableProcessors())
)

Performance Comparison by Thread Count

Single Thread Environment

Select Benchmark (1 Thread)

In a single-threaded environment, the performance difference between ldbc and jdbc is relatively small as concurrency advantages are not utilized.

Performance Ratio (ldbc/jdbc):

2 Thread Environment

Select Benchmark (2 Threads)

Starting with 2 threads, ldbc's advantages become more apparent.

Performance Ratio (ldbc/jdbc):

4 Thread Environment

Select Benchmark (4 Threads)

At 4 threads, ldbc's scalability becomes pronounced.

Performance Ratio (ldbc/jdbc):

8 Thread Environment

Select Benchmark (8 Threads)

At 8 threads, ldbc shows its highest performance advantage.

Performance Ratio (ldbc/jdbc):

16 Thread Environment

Select Benchmark (16 Threads)

Even at 16 threads, ldbc maintains stable high performance.

Performance Ratio (ldbc/jdbc):

Technical Analysis

Cats Effect Performance Characteristics

Cats Effect 3 is optimized for long-lived backend network applications:

Optimization Targets

Performance Metrics

Userspace Scheduler

  1. Work-stealing algorithm: Designed for throughput over pure responsiveness
  2. Fine-grained preemption: More flexible task switching than Kotlin coroutines
  3. Stack usage: Constant memory usage through coroutine interpreter

Comparison with Other Runtimes

Concurrency Model Differences

ldbc (Cats Effect 3)

ldbc adopts Cats Effect 3's Fiber-based concurrency model:

// Non-blocking I/O operations
for {
  statement <- connection.prepareStatement(sql)
  _         <- statement.setInt(1, id)
  resultSet <- statement.executeQuery()
  result    <- resultSet.decode[User]
} yield result

Characteristics:

jdbc (Traditional Thread Model)

jdbc uses traditional OS threads with blocking I/O:

// Blocking I/O operations
Sync[F].blocking {
  val statement = connection.prepareStatement(sql)
  statement.setInt(1, id)
  val resultSet = statement.executeQuery()
  // Thread blocks here
}

Characteristics:

Network I/O Implementation

ldbc - Non-blocking Socket

// Non-blocking reads using fs2 Socket
socket.read(8192).flatMap { chunk =>
  // Efficient chunk-based processing
  processChunk(chunk)
}

jdbc - Blocking Socket

// Traditional blocking I/O
val bytes = inputStream.read(buffer)
// Thread blocks until I/O completes

Memory Efficiency and GC Pressure

ldbc Memory Management

  1. Pre-allocated buffers: Reusable buffers for result rows
  2. Streaming processing: On-demand data fetching
  3. Immutable data structures: Efficient memory usage through structural sharing

jdbc Memory Management

  1. Bulk loading: Entire result set held in memory
  2. Intermediate objects: Boxing/unboxing overhead
  3. GC pressure: Frequent GC due to temporary objects

Usage Recommendations by Scenario

When to Choose ldbc

  1. High-Concurrency Applications

    • Web applications (high traffic)
    • Microservices
    • Real-time data processing
  2. Resource-Constrained Environments

    • Container environments (Kubernetes, etc.)
    • Serverless environments
    • Memory-limited environments
  3. Scalability Focus

    • Expected future load increases
    • Need for elastic scaling
    • Cloud-native applications
  4. Functional Programming

    • Pure functional architecture
    • Type safety emphasis
    • Composability focus

When to Choose jdbc

  1. Legacy System Integration

    • Existing jdbc codebase
    • Third-party library dependencies
    • High migration costs
  2. Simple CRUD Operations

    • Low concurrency
    • Batch processing
    • Administrative tools
  3. Special jdbc Features

    • Vendor-specific extensions
    • Special driver requirements

Performance Tuning

Cats Effect Best Practices

1. Understanding Your Workload

2. Optimizing IO Operations

// Leverage fine-grained IO composition
val optimized = for {
  data <- fetchData()           // Non-blocking I/O
  _    <- IO.cede              // Explicit cooperative yield
  processed <- processData(data) // CPU-intensive processing
  _    <- saveResult(processed) // Non-blocking I/O
} yield processed

// Execute CPU-bound tasks on dedicated pool
val cpuBound = IO.blocking {
  // Heavy computation
}.evalOn(cpuBoundExecutor)

3. Resource Management

// Safe resource management with Resource
val dataSource = Resource.make(
  createDataSource()  // Acquire
)(_.close())         // Release

// Guaranteed resource cleanup
dataSource.use { ds =>
  // Process with datasource
}

ldbc Optimization Settings

val datasource = MySQLDataSource
  .build[IO]("localhost", 3306, "user")
  .setPassword("password")
  .setDatabase("db")
  // Performance settings
  .setUseServerPrepStmts(true)      // Server-side prepared statements
  .setUseCursorFetch(true)           // Cursor-based fetching
  .setFetchSize(1000)                // Fetch size
  .setSocketOptions(List(
    SocketOption.noDelay(true),      // TCP_NODELAY
    SocketOption.keepAlive(true)     // Keep-alive
  ))
  .setReadTimeout(30.seconds)        // Read timeout

Thread Pool Configuration

// Cats Effect 3 runtime configuration
object Main extends IOApp {
  override def computeWorkerThreadCount: Int = 
    math.max(4, Runtime.getRuntime.availableProcessors())
    
  override def run(args: List[String]): IO[ExitCode] = {
    // Application logic
  }
}

Conclusion

ldbc is an excellent choice particularly when these conditions are met:

  1. High Concurrency: Need to handle many concurrent connections
  2. Scalability: Require flexible scaling based on load
  3. Resource Efficiency: Need to minimize memory usage
  4. Type Safety: Value compile-time type checking
  5. Functional Programming: Adopting pure functional architecture

As benchmark results demonstrate, ldbc achieves approximately 2x the throughput of jdbc in environments with 8+ threads, while maintaining excellent scalability with minimal performance degradation at higher thread counts.

Benefits from Cats Effect Optimization

The Cats Effect 3 runtime that ldbc leverages is optimized for:

These characteristics perfectly align with ldbc's use case as a database access library, making ldbc a powerful choice for modern cloud-native applications and high-traffic web services.

Key Principle: "Performance is always relative to your target use-case and assumptions" - we recommend measuring in your actual application context.