What Is Shared Memory in GLSL Compute Shaders?
In GLSL compute shaders, the shared memory qualifier
declares variables that are allocated in fast on-chip memory and shared
among all invocations within a single workgroup. This article explains
the role of shared memory, how it drastically reduces high-latency
global memory access, the synchronization mechanisms required to prevent
race conditions, and practical best practices for optimizing compute
shader performance.
Understanding Shared Memory Storage
When executing compute shaders in OpenGL or Vulkan, invocations are grouped into local workgroups. By default, variables declared globally or within shader storage buffer objects (SSBOs) either reside in private registers or global device memory (VRAM).
Declaring a variable with the shared storage qualifier
allocates it in high-speed, on-chip scratchpad memory (often termed
Local Data Share or Shared Memory in GPU hardware architectures).
#version 450
layout(local_size_x = 16, local_size_y = 16) in;
// Shared memory array accessible by all 256 invocations in this workgroup
shared float tileData[16][16];
void main() {
// Each invocation accesses the shared data
uint x = gl_LocalInvocationID.x;
uint y = gl_LocalInvocationID.y;
tileData[x][y] = 1.0;
}Primary Functions and Benefits
The shared memory storage qualifier provides several essential capabilities for compute pipelines:
- Inter-Thread Communication: It serves as a scratchpad allowing threads inside the same workgroup to exchange intermediate calculation results without round-tripping through global memory.
- Latency Reduction (Tiling/Caching): Global GPU
memory bandwidth is often the main performance bottleneck. Threads can
cooperatively load a patch of data (a tile) from an SSBO or texture into
sharedmemory once, reuse it multiple times for local stencils or convolutions, and avoid redundant global memory reads. - Cooperative Algorithms: It enables workgroup-wide algorithms such as parallel reductions (finding min/max/sum), prefix sums (scan operations), sorting, and matrix multiplication.
Synchronization and Memory Barriers
Because shared memory is concurrently read and written by multiple invocations running asynchronously, uncoordinated access causes data race conditions. GLSL provides built-in barrier functions to coordinate execution and memory visibility:
barrier(): Acts as an execution barrier. Every invocation in the workgroup must reach this instruction before any invocation is allowed to proceed.memoryBarrierShared(): Ensures that all previous writes tosharedvariables by the calling invocation are completed and visible to other invocations in the workgroup.groupMemoryBarrier(): Combines ordering for shared variables and all other memory accesses across the local workgroup.
In typical compute algorithms, threads load data into shared arrays,
execute barrier(), process the shared data, and often
synchronize again before writing results back to global buffers.
Limitations and Constraints
While shared memory is substantially faster than global memory, it is a finite resource:
- Capacity Limits: Hardware limits the maximum size
of shared storage (often 32 KB or 64 KB per workgroup, queryable via
GL_MAX_COMPUTE_SHARED_MEMORY_SIZE). Exceeding this limit causes shader compilation failures. - Occupancy Impact: Allocating large amounts of shared memory per workgroup limits how many active workgroups the GPU can schedule concurrently on a single compute unit or streaming multiprocessor, potentially lowering overall GPU occupancy.
- Scope Restriction: Shared variables cannot be initialized at declaration and are strictly isolated to the executing workgroup; threads in different workgroups cannot read or write to each other's shared storage.
- Bank Conflicts: Shared memory is organized into memory banks. If multiple threads within the same execution sub-group (warp or wavefront) access different memory addresses located on the same memory bank simultaneously, access is serialized, reducing throughput.