C ve C# ile VLC'nin Gaussian Blur Performansı Karşılaştırması

C ve C# ile VLC'nin Gaussian Blur Performansı Karşılaştırması
audio-thumbnail
C ve C# ile VLC'nin Gaussian Blur Performansı Karşılaştırması
0:00
/0

C ve C# ile VLC'nin Gaussian Blur Performansı Karşılaştırması

Görsel işleme, özellikle video filtreleme gibi uygulamalarda performansın kritik olduğu bir alan. VLC, bu tür işlemcilerle dolu bir medya çalma programıdır ve yıllar boyunca C dilinde geliştirilmiştir. Ancak, .NET Native AOT teknolojisi sayesinde artık C# ile VLC plugin'ları yazılabilir hale gelmiştir. Peki bu iki dil, özellikle bir Gaussian blur filtresi gibi yoğun piksel işlemlerinde, performans açısından nasıl karşılaştırılır?

Bu yazıda, C ve C# dillerinin performans açısından nasıl farklılaştığını, özellikle görsel işleme senaryolarında karşılaştıran bir analiz yapacağız. Hedef, C# ile yazılan bir filtrein neden daha yavaş olduğunu anlamak ve bu farkın ne kadar azaltılabilacağını incelemektir.


Gaussian Blur Nedir?

Gaussian blur, her pikselin etrafındaki komşu piksellerin ağırlıklı ortalaması ile değiştirilmesiyle oluşan bir bulanıklık efektidir. Bu işlem, Carl Friedrich Gauss’un 1809’da ölçüm hatalarını açıklamak için kullandığı "zili" (bell curve) ile matematiksel olarak tanımlanır. Sigma değeri, etki alanının genişliğini belirler: sigma 4, 25 piksellik bir pencere anlamına gelir. Bu, pikselin etrafındaki komşuların ağırlıklı ortalamasının alınmasıyla elde edilen bir bulanıklık efektidir.

Bu işlem, dijital görüntülemede en eski etkilerden biridir: Photoshop 1.0 (1990), GIMP, FFmpeg ve GStreamer gibi birçok yazılımında bulunur. VLC'de ise 2007 yılından beri "Spatial blur" olarak "Araçlar > Etkiler ve Filtreler > Video Etkileri > Gelişmiş" menüsünde mevcuttur.


VLC'de Gaussian Blur Nasıl Gerçekleştirilir?

Gaussian blur, ayrılabilir (separable) bir işlemdir: her satırın bir kez, ardından her sütunun bir kez işlenmesiyle tam 2D pencereyle aynı sonucu elde edilir. Bu sayede, 25x25 piksellik bir pencere (her piksel için 625 çarpma) iki adımlık işlem (her biri 25 piksel) haline gelir.

VLC, gaussianblur.c dosyasında bu işlemi tamamen tamsayılarla gerçekleştirir. İlk olarak yatay bir geçiş yapılır ve sonuç bir geçici buffer'a yazılır. Ardından bu buffer üzerinden dikey bir geçiş yapılır ve sonuçta piksellerin ağırlıklı ortalaması hesaplanır. Son olarak, toplam ağırlıklara göre bölme işlemi yapılır.

C Kodu: Yatay Geçiş

for (int i_line = 0; i_line < i_visible_lines; i_line++) {
    for (int i_col = 0; i_col < i_visible_pitch; i_col++) {
        type_t t_value = 0;
        const int c = i_line * i_in_pitch + i_col;
        for (int x = __MAX(-i_dim, -i_col * (x_factor + 1)); 
             x <= __MIN(i_dim, (i_visible_pitch - i_col) * (x_factor + 1) + 1); 
             x++) {
            t_value += pt_distribution[x + i_dim] * p_in[c + (x >> x_factor)];
        }
        pt_buffer[c] = t_value;
    }
}

Bu kod, C# ile aynı mantıkta port edilmiş ve doğrudan VLC'e entegre edilmiş bir plugin olarak çalıştırılmıştır.


C# Sürümü: Doğrudan Port

C# sürümü, orijinal C kodu ile aynı mantığı koruyarak bir döngü, aynı sınırlar, aynı tamsayı matematiği ve C'deki gibi ham pointer'lar kullanarak geliştirilmiştir:

for (int line = 0; line < visibleLines; line++) {
    for (int col = 0; col < visiblePitch; col++) {
        int value = 0;
        int c = line * inPitch + col;
        for (int x = Math.Max(-dim, -col * (xFactor + 1)); 
             x <= Math.Min(dim, (visiblePitch - col) * (xFactor + 1) + 1); 
             x++) {
            value += distribution[x + dim] * input[c + (x >> xFactor)];
        }
        buffer[c] = value;
    }
}

Bu filtre, VLCLR aracılığıyla .NET Native AOT ile derlenerek natif bir DLL olarak VLC'e yüklenir. Her kare için atama yapılmadığından, atık toplama (garbage collector) bu işlem sırasında hiçbir şey toplamaz. Her sürüm, aynı VLC 4 sürümünde, aynı çerçevelerle çalıştırılır ve çıktılar tamamen aynıdır.

Her filtre çağrısı, aynı şekilde zamanlanır. Testler, 1080p bir klip üzerinde, 11. nesil Intel Core i7 (Tiger Lake, 4 çekirdekli) bir dizüstü bilgisayarda gerçekleştirildi.


Round 1: Doğrudan Port (Line-by-Line)

İlk test, C ve C# kodlarının doğrudan port edilmiş hallerini karşılaştırır. Aşağıdaki tablo, 1080p çerçeveler için milisaniye cinsinden süreyi gösterir (daha düşük değer daha iyi demektir):

Sigma VLC (ms) C# Port (ms)
2 62.0 78.9
4 125.9 155.2
8 252.9 298.1

Gördüğümüz gibi, C# 23% daha uzun süre alıyor. Bu, beklenen bir durumdur çünkü .NET'in kod üreticisi (RyuJIT) C'teki daha optimize edilmiş makine koduna göre daha fazla ekstra işlem yapar.

Özellikle iç döngüde, Math.Min ve Math.Max gibi metodlar her adımda yeniden hesaplanıyor, bu da gereksiz bir işlem oluyor. Ayrıca, 32-bit ile 64-bit dönüşümleri her bellek erişiminde yapılırken, C'de bu dönüşümler otomatik olarak yönetilir.


Round 1 Güncellemesi (12 Ekim 2026)

Yayınlandıktan sonra, bu farkın ne kadarının kod üreticiden, ne kadarının C# kodunun kendisinden kaynaklandığını inceledik. İki küçük değişiklikle bu fark büyük ölçüde azaltıldı:

  1. Döngü sınırlarını döngü dışına çıkararak (Math.Max, Math.Min hesaplamalarını önceden yapın).
  2. Dizin türünü nint olarak kullanarak 32-bit ile 64-bit dönüşümünü kaldırmak.

Yenilenmiş C# Kodu

nint c = (nint)line * inPitch + col;
nint xStart = Math.Max(-dim, -col * (xFactor + 1));
nint xEnd = Math.Min(dim, (visiblePitch - col) * (xFactor + 1) + 1);
for (nint x = xStart; x <= xEnd; x++) {
    value += distribution[x + dim] * input[c + (x >> xFactor)];
}

Yeni sonuçlar:

Sigma VLC (ms) C# Port (Bounds Hoisted) (ms)
2 62.6 67.9
4 137.9 126.4
8 256.7 263.5

Bu değişikliklerle, C# portu C'ye %8 kadar yakın bir performansa ulaşmış ve sigma 4 için bile C'den daha hızlı çalışmıştır. Farkın büyük bir kısmı, hem C# kodunun hem de .NET compiler'ının doğrudan optimize edilmesiyle kapatılmıştır.


Round 2: Algoritma Optimizasyonu

Kodun optimize edilmesinden sonra, SIMD veya çoklu iş parçacığı gibi gelişmiş optimizasyonlara geçmeden önce, algoritma düzeyinde de önemli kazançlar elde edilebilir:

  1. Bölme yerine çarpma: Kenarlar dışındaki bölenler sabit olduğundan, önceden hesaplanmış "magik" bir ters çarpım (reciprocal) ve kaydırma ile bölme işlemi yerine daha hızlı bir çarpma kullanılır.
  2. Bellek sırasına göre erişim: Dikey geçişte her adımda bir tam satır atlanıyor. Ancak, tam satırların biriktirilmesiyle bellek sırasına göre erişim sağlanır ve performans artar.
  3. Zarafetin kullanımı: Gauss eğrisinin simetrisinden yararlanarak w*a + w*b = w*(a+b) şeklinde çarpmalar yarıya indirilir.
  4. Kenarlar ayrı ele alınır: İç kısımlar daha basit döngülerle işlenirken, kenarlar özel olarak ele alınır. Bu, sınırların kontrol edilmesi gereksinimini azaltır.

Bu optimizasyonlarla, hem C hem de C# kodları daha da hızlanabilir. Yine de, ilk iki adımın bile büyük bir fark yarattığı görülmektedir.


Sonuç

C ve C# arasındaki performans farkı, özellikle .NET Native AOT ile birlikte geliştirilen yüksek performanslı uygulamalarda çok daha azdır. Başlangıçta C# 23% daha yavaş çıkarken, iki küçük kod değişikliğiyle bu fark %8'e kadar indirildi. Bu, C#’ın artık VLC gibi performans kritik uygulamalarda C ile rekabet edebilecek bir dil olduğunu gösterir.

Ancak, C#'ın bu performansı elde etmesi için, dil kütüphanesinin optimize edilmiş yapılandırılması, yönetilen kodun optimize edilmesi ve dışarıdan erişim için dikkatli tasarım gereklidir. Bu durum, C# ile yazılan plugin'lerin artık sadece "çalışır" olmakla kalmayıp, aynı zamanda "hızlı" da olabileceğini göstermektedir.

Bu analiz, C# ile C arasındaki performans farkının daha çok kodun nasıl yazıldığına bağlı olduğunu, değil de dilin kendisine bağlı olduğunu ortaya koymaktadır.