Eu executei alguns testes com um bit de lógica de longa execução, com o mesmo pedaço de código (uma instrução SELECT longa) em execução em uma Função com valor de tabela e em um procedimento armazenado, e um EXEC / SELECT direto, e cada um executado de forma idêntica.
Na minha opinião, sempre use uma função com valor de tabela em vez de um procedimento armazenado para retornar um conjunto de resultados, pois torna a lógica muito mais fácil e legível em consultas que subsequentemente se unem a eles e permite que você reutilize a mesma lógica. Para evitar um impacto excessivo no desempenho, costumo usar parâmetros "opcionais" (ou seja, você pode passar NULL para eles) para permitir que a função retorne o conjunto de resultados mais rápido, por exemplo:
CREATE FUNCTION dbo.getSitePermissions(@RegionID int, @optPersonID int, optSiteID int)
AS
RETURN
SELECT DISTINCT SiteID, PersonID
FROM dbo.SiteViewPermissions
WHERE (@optPersonID IS NULL OR @optPersonID = PersonID)
AND (@optSiteID IS NULL OR @optSiteID = SiteID)
AND @RegionID = RegionID
Desta forma, você pode usar esta função para muitas situações diferentes e não terá um grande impacto no desempenho. Acredito que isso seja mais eficiente do que filtrar depois:
SELECT * FROM dbo.getSitePermissions(@RegionID) WHERE SiteID = 1
Usei essa técnica em várias funções, às vezes com uma longa lista de parâmetros "opcionais" desse tipo.